From ac3888f7c842f8fc373e96cf4390dd1c873c8c4c Mon Sep 17 00:00:00 2001 From: James Brooks Date: Sun, 10 Jan 2016 15:54:54 +0000 Subject: [PATCH] Added per-component subscriptions. Closes #734 --- .../Subscriber/SubscribeSubscriberCommand.php | 20 +++- .../UnsubscribeSubscriptionCommand.php | 36 ++++++ ...SubscriberHasUpdatedSubscriptionsEvent.php | 41 +++++++ .../SubscribeSubscriberCommandHandler.php | 30 ++++- .../UnsubscribeSubscriptionCommandHandler.php | 35 ++++++ .../VerifySubscriberCommandHandler.php | 1 + ...omponentUpdateEmailNotificationHandler.php | 72 +++++++++++ .../Providers/EventServiceProvider.php | 5 +- .../Providers/RouteServiceProvider.php | 1 + .../Controllers/Api/SubscriberController.php | 18 ++- app/Http/Controllers/SubscribeController.php | 19 ++- app/Http/Routes/ApiRoutes.php | 1 + app/Http/Routes/SubscribeRoutes.php | 2 +- app/Models/Subscriber.php | 10 ++ app/Models/Subscription.php | 113 ++++++++++++++++++ database/factories/ModelFactory.php | 11 ++ ..._01_09_141852_CreateSubscriptionsTable.php | 42 +++++++ resources/assets/js/app.js | 17 +++ resources/lang/en/cachet.php | 17 +++ .../emails/components/update-html.blade.php | 16 +++ .../emails/components/update-text.blade.php | 7 ++ resources/views/index.blade.php | 2 + resources/views/partials/component.blade.php | 4 + .../views/partials/modals/subscribe.blade.php | 25 ++++ tests/Api/SubscriberTest.php | 9 ++ .../SubscribeSubscriberCommandTest.php | 4 +- .../UnsubscribeSubscriptionCommandTest.php | 41 +++++++ .../ComponentWasUpdatedEventTest.php | 2 +- ...criberHasUpdatedSubscriptionsEventTest.php | 36 ++++++ 29 files changed, 620 insertions(+), 17 deletions(-) create mode 100644 app/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommand.php create mode 100644 app/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEvent.php create mode 100644 app/Bus/Handlers/Commands/Subscriber/UnsubscribeSubscriptionCommandHandler.php create mode 100644 app/Bus/Handlers/Events/Component/SendComponentUpdateEmailNotificationHandler.php create mode 100644 app/Models/Subscription.php create mode 100644 database/migrations/2016_01_09_141852_CreateSubscriptionsTable.php create mode 100644 resources/views/emails/components/update-html.blade.php create mode 100644 resources/views/emails/components/update-text.blade.php create mode 100644 resources/views/partials/modals/subscribe.blade.php create mode 100644 tests/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommandTest.php create mode 100644 tests/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEventTest.php diff --git a/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php b/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php index 61271928..b7be8986 100644 --- a/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php +++ b/app/Bus/Commands/Subscriber/SubscribeSubscriberCommand.php @@ -11,6 +11,11 @@ namespace CachetHQ\Cachet\Bus\Commands\Subscriber; +/** + * This is the subscribe subscriber command. + * + * @author James Brooks + */ final class SubscribeSubscriberCommand { /** @@ -27,6 +32,13 @@ final class SubscribeSubscriberCommand */ public $verified; + /** + * The subscriptions that we want to add. + * + * @var array|null + */ + public $subscriptions; + /** * The validation rules. * @@ -39,14 +51,16 @@ final class SubscribeSubscriberCommand /** * Create a new subscribe subscriber command instance. * - * @param string $email - * @param bool $verified + * @param string $email + * @param bool $verified + * @param null|array $subscriptions * * @return void */ - public function __construct($email, $verified = false) + public function __construct($email, $verified = false, $subscriptions = null) { $this->email = $email; $this->verified = $verified; + $this->subscriptions = $subscriptions; } } diff --git a/app/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommand.php b/app/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommand.php new file mode 100644 index 00000000..67c297a1 --- /dev/null +++ b/app/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommand.php @@ -0,0 +1,36 @@ +subscription = $subscription; + } +} diff --git a/app/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEvent.php b/app/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEvent.php new file mode 100644 index 00000000..12f6e1ec --- /dev/null +++ b/app/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEvent.php @@ -0,0 +1,41 @@ + + */ +final class SubscriberHasUpdatedSubscriptionsEvent implements SubscriberEventInterface +{ + /** + * The subscriber. + * + * @var \CachetHQ\Cachet\Models\Subscriber + */ + public $subscriber; + + /** + * Create a new subscriber has updated subscriptions event instance. + * + * @param \CachetHQ\Cachet\Models\Subscriber $subscriber + * + * @return void + */ + public function __construct(Subscriber $subscriber) + { + $this->subscriber = $subscriber; + } +} diff --git a/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php b/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php index 12238268..309d79c0 100644 --- a/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php +++ b/app/Bus/Handlers/Commands/Subscriber/SubscribeSubscriberCommandHandler.php @@ -14,9 +14,16 @@ namespace CachetHQ\Cachet\Bus\Handlers\Commands\Subscriber; use CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeSubscriberCommand; use CachetHQ\Cachet\Bus\Commands\Subscriber\VerifySubscriberCommand; use CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasSubscribedEvent; +use CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasUpdatedSubscriptionsEvent; use CachetHQ\Cachet\Bus\Exceptions\Subscriber\AlreadySubscribedException; use CachetHQ\Cachet\Models\Subscriber; +use CachetHQ\Cachet\Models\Subscription; +/** + * This is the subscribe subscriber command handler. + * + * @author James Brooks + */ class SubscribeSubscriberCommandHandler { /** @@ -30,16 +37,29 @@ class SubscribeSubscriberCommandHandler */ public function handle(SubscribeSubscriberCommand $command) { - if (Subscriber::where('email', $command->email)->first()) { + if (Subscriber::where('email', $command->email)->first() && $command->subscriptions === null) { throw new AlreadySubscribedException("Cannot subscribe {$command->email} because they're already subscribed."); } - $subscriber = Subscriber::create(['email' => $command->email]); + $subscriber = Subscriber::firstOrCreate(['email' => $command->email]); - if ($command->verified) { - dispatch(new VerifySubscriberCommand($subscriber)); + if ($subscriptions = $command->subscriptions) { + foreach ($subscriptions as $subscription => $subscriptionValue) { + Subscription::firstOrCreate([ + 'subscriber_id' => $subscriber->id, + $subscription => $subscriptionValue, + ]); + } + } + + if ($subscriber->is_verified === false) { + if ($command->verified) { + dispatch(new VerifySubscriberCommand($subscriber)); + } else { + event(new SubscriberHasSubscribedEvent($subscriber)); + } } else { - event(new SubscriberHasSubscribedEvent($subscriber)); + event(new SubscriberHasUpdatedSubscriptionsEvent($subscriber)); } return $subscriber; diff --git a/app/Bus/Handlers/Commands/Subscriber/UnsubscribeSubscriptionCommandHandler.php b/app/Bus/Handlers/Commands/Subscriber/UnsubscribeSubscriptionCommandHandler.php new file mode 100644 index 00000000..55869119 --- /dev/null +++ b/app/Bus/Handlers/Commands/Subscriber/UnsubscribeSubscriptionCommandHandler.php @@ -0,0 +1,35 @@ +subscription; + + event(new SubscriberHasUnsubscribedEvent($subscription->subscriber)); + + $subscription->delete(); + } +} diff --git a/app/Bus/Handlers/Commands/Subscriber/VerifySubscriberCommandHandler.php b/app/Bus/Handlers/Commands/Subscriber/VerifySubscriberCommandHandler.php index 961f274a..b5c104f6 100644 --- a/app/Bus/Handlers/Commands/Subscriber/VerifySubscriberCommandHandler.php +++ b/app/Bus/Handlers/Commands/Subscriber/VerifySubscriberCommandHandler.php @@ -29,6 +29,7 @@ class VerifySubscriberCommandHandler { $subscriber = $command->subscriber; + // Mark the subscriber as verified. $subscriber->verified_at = Carbon::now(); $subscriber->save(); diff --git a/app/Bus/Handlers/Events/Component/SendComponentUpdateEmailNotificationHandler.php b/app/Bus/Handlers/Events/Component/SendComponentUpdateEmailNotificationHandler.php new file mode 100644 index 00000000..80f88e3b --- /dev/null +++ b/app/Bus/Handlers/Events/Component/SendComponentUpdateEmailNotificationHandler.php @@ -0,0 +1,72 @@ +mailer = $mailer; + } + + /** + * Handle the event. + * + * @param \CachetHQ\Cachet\Bus\Events\Component\ComponentWasUpdatedEvent $event + * + * @return void + */ + public function handle(ComponentWasUpdatedEvent $event) + { + $component = AutoPresenter::decorate($event->component); + + $mail = [ + 'subject' => trans('cachet.subscriber.email.component.subject'), + 'component_name' => $component->name, + 'component_human_status' => $component->human_status, + ]; + + foreach (Subscription::isVerifiedForComponent($component->id)->with('subscriber')->get() as $subscription) { + $subscriber = $subscription->subscriber; + $mail['email'] = $subscriber->email; + $mail['unsubscribe_link'] = route('subscribe.unsubscribe', ['code' => $subscriber->verify_code, 'subscription' => $subscription->id]); + + $this->mailer->queue([ + 'html' => 'emails.components.update-html', + 'text' => 'emails.components.update-text', + ], $mail, function (Message $message) use ($mail) { + $message->to($mail['email'])->subject($mail['subject']); + }); + } + } +} diff --git a/app/Foundation/Providers/EventServiceProvider.php b/app/Foundation/Providers/EventServiceProvider.php index 183feb86..f9f02494 100644 --- a/app/Foundation/Providers/EventServiceProvider.php +++ b/app/Foundation/Providers/EventServiceProvider.php @@ -37,7 +37,7 @@ class EventServiceProvider extends ServiceProvider // ], 'CachetHQ\Cachet\Bus\Events\Component\ComponentWasUpdatedEvent' => [ - // + 'CachetHQ\Cachet\Bus\Handlers\Events\Component\SendComponentUpdateEmailNotificationHandler', ], 'CachetHQ\Cachet\Bus\Events\Incident\IncidentWasReportedEvent' => [ 'CachetHQ\Cachet\Bus\Handlers\Events\Incident\SendIncidentEmailNotificationHandler', @@ -72,6 +72,9 @@ class EventServiceProvider extends ServiceProvider 'CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasUnsubscribedEvent' => [ // ], + 'CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasUpdatedSubscriptionsEvent' => [ + // + ], 'CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasVerifiedEvent' => [ // ], diff --git a/app/Foundation/Providers/RouteServiceProvider.php b/app/Foundation/Providers/RouteServiceProvider.php index 6ad62b82..f25c7ae3 100644 --- a/app/Foundation/Providers/RouteServiceProvider.php +++ b/app/Foundation/Providers/RouteServiceProvider.php @@ -54,6 +54,7 @@ class RouteServiceProvider extends ServiceProvider $this->app->router->model('metric_point', 'CachetHQ\Cachet\Models\MetricPoint'); $this->app->router->model('setting', 'CachetHQ\Cachet\Models\Setting'); $this->app->router->model('subscriber', 'CachetHQ\Cachet\Models\Subscriber'); + $this->app->router->model('subscription', 'CachetHQ\Cachet\Models\Subscription'); $this->app->router->model('user', 'CachetHQ\Cachet\Models\User'); } diff --git a/app/Http/Controllers/Api/SubscriberController.php b/app/Http/Controllers/Api/SubscriberController.php index 279b3384..66c2f53e 100644 --- a/app/Http/Controllers/Api/SubscriberController.php +++ b/app/Http/Controllers/Api/SubscriberController.php @@ -13,7 +13,9 @@ namespace CachetHQ\Cachet\Http\Controllers\Api; use CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeSubscriberCommand; use CachetHQ\Cachet\Bus\Commands\Subscriber\UnsubscribeSubscriberCommand; +use CachetHQ\Cachet\Bus\Commands\Subscriber\UnsubscribeSubscriptionCommand; use CachetHQ\Cachet\Models\Subscriber; +use CachetHQ\Cachet\Models\Subscription; use GrahamCampbell\Binput\Facades\Binput; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Request; @@ -41,7 +43,7 @@ class SubscriberController extends AbstractApiController public function postSubscribers() { try { - $subscriber = dispatch(new SubscribeSubscriberCommand(Binput::get('email'), Binput::get('verify', false))); + $subscriber = dispatch(new SubscribeSubscriberCommand(Binput::get('email'), Binput::get('verify', false), null)); } catch (QueryException $e) { throw new BadRequestHttpException(); } @@ -62,4 +64,18 @@ class SubscriberController extends AbstractApiController return $this->noContent(); } + + /** + * Delete a subscriber. + * + * @param \CachetHQ\Cachet\Models\Subscriber $subscriber + * + * @return \Illuminate\Http\JsonResponse + */ + public function deleteSubscription(Subscription $subscriber) + { + dispatch(new UnsubscribeSubscriptionCommand($subscriber)); + + return $this->noContent(); + } } diff --git a/app/Http/Controllers/SubscribeController.php b/app/Http/Controllers/SubscribeController.php index 43085729..105b92e6 100644 --- a/app/Http/Controllers/SubscribeController.php +++ b/app/Http/Controllers/SubscribeController.php @@ -14,10 +14,12 @@ namespace CachetHQ\Cachet\Http\Controllers; use AltThree\Validator\ValidationException; use CachetHQ\Cachet\Bus\Commands\Subscriber\SubscribeSubscriberCommand; use CachetHQ\Cachet\Bus\Commands\Subscriber\UnsubscribeSubscriberCommand; +use CachetHQ\Cachet\Bus\Commands\Subscriber\UnsubscribeSubscriptionCommand; use CachetHQ\Cachet\Bus\Commands\Subscriber\VerifySubscriberCommand; use CachetHQ\Cachet\Bus\Exceptions\Subscriber\AlreadySubscribedException; use CachetHQ\Cachet\Facades\Setting; use CachetHQ\Cachet\Models\Subscriber; +use CachetHQ\Cachet\Models\Subscription; use GrahamCampbell\Binput\Facades\Binput; use GrahamCampbell\Markdown\Facades\Markdown; use Illuminate\Routing\Controller; @@ -26,6 +28,11 @@ use Illuminate\Support\Facades\View; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +/** + * This is the subscribe controller. + * + * @author James Brooks + */ class SubscribeController extends Controller { /** @@ -47,9 +54,10 @@ class SubscribeController extends Controller public function postSubscribe() { $email = Binput::get('email'); + $subscriptions = Binput::get('subscriptions'); try { - dispatch(new SubscribeSubscriberCommand($email)); + dispatch(new SubscribeSubscriberCommand($email, false, $subscriptions)); } catch (AlreadySubscribedException $e) { return Redirect::route('subscribe.subscribe') ->withTitle(sprintf('%s %s', trans('dashboard.notifications.whoops'), trans('cachet.subscriber.email.failure'))) @@ -94,10 +102,11 @@ class SubscribeController extends Controller * Handle the unsubscribe. * * @param string|null $code + * @param int|null $subscription * * @return \Illuminate\View\View */ - public function getUnsubscribe($code = null) + public function getUnsubscribe($code = null, $subscription = null) { if ($code === null) { throw new NotFoundHttpException(); @@ -109,7 +118,11 @@ class SubscribeController extends Controller throw new BadRequestHttpException(); } - dispatch(new UnsubscribeSubscriberCommand($subscriber)); + if ($subscription) { + dispatch(new UnsubscribeSubscriptionCommand(Subscription::forSubscriber($subscriber->id)->firstOrFail())); + } else { + dispatch(new UnsubscribeSubscriberCommand($subscriber, $subscription)); + } return Redirect::route('status-page') ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('cachet.subscriber.email.unsubscribed'))); diff --git a/app/Http/Routes/ApiRoutes.php b/app/Http/Routes/ApiRoutes.php index b3681dcd..228e9c7e 100644 --- a/app/Http/Routes/ApiRoutes.php +++ b/app/Http/Routes/ApiRoutes.php @@ -68,6 +68,7 @@ class ApiRoutes $router->delete('metrics/{metric}', 'MetricController@deleteMetric'); $router->delete('metrics/{metric}/points/{metric_point}', 'MetricPointController@deleteMetricPoint'); $router->delete('subscribers/{subscriber}', 'SubscriberController@deleteSubscriber'); + $router->delete('subscriptions/{subscription}', 'SubscriberController@deleteSubscription'); }); }); } diff --git a/app/Http/Routes/SubscribeRoutes.php b/app/Http/Routes/SubscribeRoutes.php index 655915ba..75384ebd 100644 --- a/app/Http/Routes/SubscribeRoutes.php +++ b/app/Http/Routes/SubscribeRoutes.php @@ -46,7 +46,7 @@ class SubscribeRoutes 'uses' => 'SubscribeController@getVerify', ]); - $router->get('unsubscribe/{code}', [ + $router->get('unsubscribe/{code}/{subscription?}', [ 'as' => 'unsubscribe', 'uses' => 'SubscribeController@getUnsubscribe', ]); diff --git a/app/Models/Subscriber.php b/app/Models/Subscriber.php index c8ed51c8..961051e3 100644 --- a/app/Models/Subscriber.php +++ b/app/Models/Subscriber.php @@ -62,6 +62,16 @@ class Subscriber extends Model implements HasPresenter }); } + /** + * A subscriber has many subscriptions. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function subcriptions() + { + return $this->hasMany(Subscription::class); + } + /** * Scope verified subscribers. * diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php new file mode 100644 index 00000000..36d536fd --- /dev/null +++ b/app/Models/Subscription.php @@ -0,0 +1,113 @@ + 'int', + 'component_id' => 'int', + ]; + + /** + * The fillable properties. + * + * @var string[] + */ + protected $fillable = [ + 'subscriber_id', + 'component_id', + ]; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'subscriber_id' => 'int|required', + 'component_id' => 'int', + ]; + + /** + * A subscription belongs to a subscriber. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function subscriber() + { + return $this->belongsTo(Subscriber::class); + } + + /** + * A subscription has one component. + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function component() + { + return $this->belongsTo(Component::class); + } + + /** + * Finds all subscriptions for a given subscriber. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $subscriber_id + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForSubscriber(Builder $query, $subscriber_id) + { + return $query->where('subscriber_id', $subscriber_id); + } + + /** + * Finds all subscriptions for a component. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $component_id + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeForComponent(Builder $query, $component_id) + { + return $query->where('component_id', $component_id); + } + + /** + * Finds all verified subscriptions for a component. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param int $component_id + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeIsVerifiedForComponent(Builder $query, $component_id) + { + return $query->select('subscriptions.*') + ->join('subscribers', 'subscriptions.subscriber_id', '=', 'subscribers.id') + ->where('component_id', $component_id) + ->whereNotNull('subscribers.verified_at'); + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 05c8720d..380e7555 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -16,6 +16,7 @@ use CachetHQ\Cachet\Models\IncidentTemplate; use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\MetricPoint; use CachetHQ\Cachet\Models\Subscriber; +use CachetHQ\Cachet\Models\Subscription; use CachetHQ\Cachet\Models\User; use Carbon\Carbon; @@ -79,6 +80,16 @@ $factory->define(Subscriber::class, function ($faker) { ]; }); +$factory->define(Subscription::class, function ($faker) { + $user = factory(Subscriber::class)->create(); + $component = factory(Component::class)->create(); + + return [ + 'subscriber_id' => $user->id, + 'component_id' => $component->id, + ]; +}); + $factory->define(User::class, function ($faker) { return [ 'username' => $faker->userName, diff --git a/database/migrations/2016_01_09_141852_CreateSubscriptionsTable.php b/database/migrations/2016_01_09_141852_CreateSubscriptionsTable.php new file mode 100644 index 00000000..f43ec71c --- /dev/null +++ b/database/migrations/2016_01_09_141852_CreateSubscriptionsTable.php @@ -0,0 +1,42 @@ +increments('id'); + $table->integer('subscriber_id')->unsigned()->index(); + $table->integer('component_id')->unsigned()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('subscriptions'); + } +} diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 5bc9c04d..8bb6ee74 100755 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -376,6 +376,23 @@ $(function() { } }); } + + // Open a modal. + $('#subscribe-modal') + .on('show.bs.modal', function (event) { + var $button = $(event.relatedTarget); + var $modal = $(this); + $modal.find('#subscribe-modal-id').val($button.data('component-id')); + }) + .on('hidden.bs.modal', function (event) { + var $modal = $(this); + $modal.find('#subscribe-modal-id').val(''); + }); + + // Focus on any modals. + $('.modal').on('shown.bs.modal', function () { + $(this).find('input[type=text]').focus(); + }); }); function askConfirmation(callback) { diff --git a/resources/lang/en/cachet.php b/resources/lang/en/cachet.php index 99ad7a37..fba30900 100755 --- a/resources/lang/en/cachet.php +++ b/resources/lang/en/cachet.php @@ -86,6 +86,13 @@ return [ 'html-preheader' => 'New incident has been reported on :app_name.', 'html' => '

New incident has been reported on :app_name.

Thank you, :app_name

', ], + 'component' => [ + 'subject' => 'Component Status Update', + 'text' => 'The component :component_name has seen a status change. The component is now at :component_human_status.\nThank you, :app_name', + 'html-preheader' => 'Component Update from :app_name', + 'html' => '

The component :component_name has seen a status change. The component is now at :component_human_status.

Thank you, :app_name

', + 'tooltip-title' => 'Subscribe to notifications for :component_name.', + ], ], ], @@ -112,6 +119,16 @@ return [ 'update' => 'There is a newer version of Cachet available. You can learn how to update here!', ], + // Modal + 'modal' => [ + 'close' => 'Close', + 'subscribe' => [ + 'title' => 'Subscribe to component updates?', + 'body' => 'Enter your email address to subscribe to updates for this component. If you\'re already subscribed, you\'ll receive emails for this component too.', + 'button' => 'Subscribe', + ], + ], + // Other 'powered_by' => ':app Status Page is powered by Cachet.', 'about_this_site' => 'About This Site', diff --git a/resources/views/emails/components/update-html.blade.php b/resources/views/emails/components/update-html.blade.php new file mode 100644 index 00000000..f8266460 --- /dev/null +++ b/resources/views/emails/components/update-html.blade.php @@ -0,0 +1,16 @@ +@extends('layout.emails') + +@section('preheader') +{!! trans('cachet.subscriber.email.component.html-preheader', ['app_name' => $app_name]) !!} +@stop + +@section('content') +{!! trans('cachet.subscriber.email.component.html', ['component_name' => $component_name, 'component_human_status' => $component_human_status, 'app_name' => $app_name]) !!} + +@if($show_support) +

{!! trans('cachet.powered_by', ['app' => $app_name]) !!}

+@endif +

+ {!! trans('cachet.subscriber.email.unsubscribe') !!} +

+@stop diff --git a/resources/views/emails/components/update-text.blade.php b/resources/views/emails/components/update-text.blade.php new file mode 100644 index 00000000..297287e4 --- /dev/null +++ b/resources/views/emails/components/update-text.blade.php @@ -0,0 +1,7 @@ +{!! trans('cachet.subscriber.email.component.text', ['component_name' => $component_name, 'component_human_status' => $component_human_status, 'app_name' => $app_name]) !!} + +@if($show_support) +{!! trans('cachet.powered_by', ['app' => $app_name]) !!} +@endif + +{!! trans('cachet.subscriber.email.unsubscribe') !!} {{ $unsubscribe_link }} diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index c935c142..72ff0ca5 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -56,4 +56,6 @@ @endif + +@include('partials.modals.subscribe') @stop diff --git a/resources/views/partials/component.blade.php b/resources/views/partials/component.blade.php index 5a941b2f..a0738d8c 100644 --- a/resources/views/partials/component.blade.php +++ b/resources/views/partials/component.blade.php @@ -9,6 +9,10 @@ @endif + @if(subscribers_enabled()) + + @endif +
{{ $component->human_status }}
diff --git a/resources/views/partials/modals/subscribe.blade.php b/resources/views/partials/modals/subscribe.blade.php new file mode 100644 index 00000000..ab6aaab0 --- /dev/null +++ b/resources/views/partials/modals/subscribe.blade.php @@ -0,0 +1,25 @@ + diff --git a/tests/Api/SubscriberTest.php b/tests/Api/SubscriberTest.php index 5651e8a2..ed3d18f3 100644 --- a/tests/Api/SubscriberTest.php +++ b/tests/Api/SubscriberTest.php @@ -72,4 +72,13 @@ class SubscriberTest extends AbstractApiTestCase $this->delete("/api/v1/subscribers/{$subscriber->id}"); $this->assertResponseStatus(204); } + + public function testDeleteSubscription() + { + $this->beUser(); + + $subscription = factory('CachetHQ\Cachet\Models\Subscription')->create(); + $this->delete("/api/v1/subscriptions/{$subscription->id}"); + $this->assertResponseStatus(204); + } } diff --git a/tests/Bus/Commands/Subscriber/SubscribeSubscriberCommandTest.php b/tests/Bus/Commands/Subscriber/SubscribeSubscriberCommandTest.php index 0a7205ab..55287bdb 100644 --- a/tests/Bus/Commands/Subscriber/SubscribeSubscriberCommandTest.php +++ b/tests/Bus/Commands/Subscriber/SubscribeSubscriberCommandTest.php @@ -28,8 +28,8 @@ class SubscribeSubscriberCommandTest extends AbstractTestCase protected function getObjectAndParams() { - $params = ['email' => 'support@cachethq.io', 'verified' => true]; - $object = new SubscribeSubscriberCommand($params['email'], $params['verified']); + $params = ['email' => 'support@cachethq.io', 'verified' => true, 'subscriptions' => null]; + $object = new SubscribeSubscriberCommand($params['email'], $params['verified'], $params['subscriptions']); return compact('params', 'object'); } diff --git a/tests/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommandTest.php b/tests/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommandTest.php new file mode 100644 index 00000000..082c6674 --- /dev/null +++ b/tests/Bus/Commands/Subscriber/UnsubscribeSubscriptionCommandTest.php @@ -0,0 +1,41 @@ + + */ +class UnsubscribeSubscriptionCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = ['subscription' => new Subscription()]; + $object = new UnsubscribeSubscriptionCommand($params['subscription']); + + return compact('params', 'object'); + } + + protected function getHandlerClass() + { + return UnsubscribeSubscriptionCommandHandler::class; + } +} diff --git a/tests/Bus/Events/Component/ComponentWasUpdatedEventTest.php b/tests/Bus/Events/Component/ComponentWasUpdatedEventTest.php index 54d6a0fd..44f88864 100644 --- a/tests/Bus/Events/Component/ComponentWasUpdatedEventTest.php +++ b/tests/Bus/Events/Component/ComponentWasUpdatedEventTest.php @@ -18,7 +18,7 @@ class ComponentWasUpdatedEventTest extends AbstractComponentEventTestCase { protected function objectHasHandlers() { - return false; + return true; } protected function getObjectAndParams() diff --git a/tests/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEventTest.php b/tests/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEventTest.php new file mode 100644 index 00000000..b0ecf6d7 --- /dev/null +++ b/tests/Bus/Events/Subscriber/SubscriberHasUpdatedSubscriptionsEventTest.php @@ -0,0 +1,36 @@ + + */ +class SubscriberHasUpdatedSubscriptionsEventTest extends AbstractSubscriberEventTestCase +{ + protected function objectHasHandlers() + { + return false; + } + + protected function getObjectAndParams() + { + $params = ['subscriber' => new Subscriber()]; + $object = new SubscriberHasUpdatedSubscriptionsEvent($params['subscriber']); + + return compact('params', 'object'); + } +}