diff --git a/app/Bus/Commands/Incident/ReportMaintenanceCommand.php b/app/Bus/Commands/Incident/ReportMaintenanceCommand.php deleted file mode 100644 index 396f88b7278b..000000000000 --- a/app/Bus/Commands/Incident/ReportMaintenanceCommand.php +++ /dev/null @@ -1,73 +0,0 @@ - 'required|string', - 'message' => 'required|string', - 'notify' => 'nullable|bool', - 'timestamp' => 'required|string', - ]; - - /** - * Create a new report maintenance command instance. - * - * @param string $name - * @param string $message - * @param bool $notify - * @param string $timestamp - * - * @return void - */ - public function __construct($name, $message, $notify, $timestamp) - { - $this->name = $name; - $this->message = $message; - $this->notify = $notify; - $this->timestamp = $timestamp; - } -} diff --git a/app/Bus/Commands/Schedule/CreateScheduleCommand.php b/app/Bus/Commands/Schedule/CreateScheduleCommand.php new file mode 100644 index 000000000000..4268e68d2e36 --- /dev/null +++ b/app/Bus/Commands/Schedule/CreateScheduleCommand.php @@ -0,0 +1,98 @@ + + */ +final class CreateScheduleCommand +{ + /** + * The schedule name. + * + * @var string + */ + public $name; + + /** + * The schedule message. + * + * @var string + */ + public $message; + + /** + * The schedule status. + * + * @var int + */ + public $status; + + /** + * The schedule date. + * + * @var string + */ + public $scheduled_at; + + /** + * The completed at date. + * + * @var string + */ + public $completed_at; + + /** + * The components affected by the schedule. + * + * @var array + */ + public $components; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'name' => 'required|string', + 'message' => 'nullable|string', + 'status' => 'required|int|min:0|max:2', + 'scheduled_at' => 'required|string', + 'completed_at' => 'nullable|string', + 'components' => 'required|array', + ]; + + /** + * Create a new create schedule command instance. + * + * @param string $name + * @param string $message + * @param int $status + * @param string $scheduled_at + * @param string $completed_at + * @param array $components + * + * @return void + */ + public function __construct($name, $message, $status, $scheduled_at, $completed_at, array $components) + { + $this->name = $name; + $this->message = $message; + $this->status = $status; + $this->scheduled_at = $scheduled_at; + $this->completed_at = $completed_at; + $this->components = $components; + } +} diff --git a/app/Bus/Commands/Schedule/DeleteScheduleCommand.php b/app/Bus/Commands/Schedule/DeleteScheduleCommand.php new file mode 100644 index 000000000000..572dcf52d805 --- /dev/null +++ b/app/Bus/Commands/Schedule/DeleteScheduleCommand.php @@ -0,0 +1,50 @@ + + */ +final class DeleteScheduleCommand +{ + /** + * The schedule to delete. + * + * @var \CachetHQ\Cachet\Models\Schedule + */ + public $schedule; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'schedule' => 'required', + ]; + + /** + * Create a new delete schedule command instance. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return void + */ + public function __construct(Schedule $schedule) + { + $this->schedule = $schedule; + } +} diff --git a/app/Bus/Commands/Schedule/UpdateScheduleCommand.php b/app/Bus/Commands/Schedule/UpdateScheduleCommand.php new file mode 100644 index 000000000000..bfe100d47a80 --- /dev/null +++ b/app/Bus/Commands/Schedule/UpdateScheduleCommand.php @@ -0,0 +1,110 @@ + + */ +final class UpdateScheduleCommand +{ + /** + * The schedule to update. + * + * @param \CachetHQ\Cachet\Models\Schedule + */ + public $schedule; + + /** + * The schedule name. + * + * @var string + */ + public $name; + + /** + * The schedule message. + * + * @var string + */ + public $message; + + /** + * The schedule status. + * + * @var int + */ + public $status; + + /** + * The schedule date. + * + * @var string + */ + public $scheduled_at; + + /** + * The completed at date. + * + * @var string + */ + public $completed_at; + + /** + * The components affected by the schedule. + * + * @var array + */ + public $components; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'schedule' => 'required', + 'name' => 'nullable|string', + 'message' => 'nullable|string', + 'status' => 'nullable|int|min:0|max:2', + 'scheduled_at' => 'nullable|string', + 'completed_at' => 'nullable|string', + 'components' => 'nullable|array', + ]; + + /** + * Create a new update schedule command instance. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * @param string $name + * @param string $message + * @param int $status + * @param string $scheduled_at + * @param string $completed_at + * @param array $components + * + * @return void + */ + public function __construct(Schedule $schedule, $name, $message, $status, $scheduled_at, $completed_at, array $components = []) + { + $this->schedule = $schedule; + $this->name = $name; + $this->message = $message; + $this->status = $status; + $this->scheduled_at = $scheduled_at; + $this->completed_at = $completed_at; + $this->components = $components; + } +} diff --git a/app/Bus/Events/Incident/MaintenanceWasScheduledEvent.php b/app/Bus/Events/Incident/MaintenanceWasScheduledEvent.php deleted file mode 100644 index 2bf293817628..000000000000 --- a/app/Bus/Events/Incident/MaintenanceWasScheduledEvent.php +++ /dev/null @@ -1,36 +0,0 @@ -incident = $incident; - } -} diff --git a/app/Bus/Events/Schedule/ScheduleEventInterface.php b/app/Bus/Events/Schedule/ScheduleEventInterface.php new file mode 100644 index 000000000000..cd23ed511bc6 --- /dev/null +++ b/app/Bus/Events/Schedule/ScheduleEventInterface.php @@ -0,0 +1,24 @@ + + */ +interface ScheduleEventInterface extends EventInterface +{ + // +} diff --git a/app/Bus/Events/Schedule/ScheduleWasCreatedEvent.php b/app/Bus/Events/Schedule/ScheduleWasCreatedEvent.php new file mode 100644 index 000000000000..32032543c79d --- /dev/null +++ b/app/Bus/Events/Schedule/ScheduleWasCreatedEvent.php @@ -0,0 +1,41 @@ + + */ +final class ScheduleWasCreatedEvent implements ScheduleEventInterface +{ + /** + * The schedule that has been created. + * + * @var \CachetHQ\Cachet\Models\Schedule + */ + public $schedule; + + /** + * Create a new schedule was created event instance. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return void + */ + public function __construct(Schedule $schedule) + { + $this->schedule = $schedule; + } +} diff --git a/app/Bus/Events/Schedule/ScheduleWasRemovedEvent.php b/app/Bus/Events/Schedule/ScheduleWasRemovedEvent.php new file mode 100644 index 000000000000..1f474afab735 --- /dev/null +++ b/app/Bus/Events/Schedule/ScheduleWasRemovedEvent.php @@ -0,0 +1,41 @@ + + */ +final class ScheduleWasRemovedEvent implements ScheduleEventInterface +{ + /** + * The schedule that has been removed. + * + * @var \CachetHQ\Cachet\Models\Schedule + */ + public $schedule; + + /** + * Create a new schedule was removed event instance. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return void + */ + public function __construct(Schedule $schedule) + { + $this->schedule = $schedule; + } +} diff --git a/app/Bus/Events/Schedule/ScheduleWasUpdatedEvent.php b/app/Bus/Events/Schedule/ScheduleWasUpdatedEvent.php new file mode 100644 index 000000000000..1953a7349da9 --- /dev/null +++ b/app/Bus/Events/Schedule/ScheduleWasUpdatedEvent.php @@ -0,0 +1,41 @@ + + */ +final class ScheduleWasUpdatedEvent implements ScheduleEventInterface +{ + /** + * The schedule that has been updated. + * + * @var \CachetHQ\Cachet\Models\Schedule + */ + public $schedule; + + /** + * Create a new schedule was updated event instance. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return void + */ + public function __construct(Schedule $schedule) + { + $this->schedule = $schedule; + } +} diff --git a/app/Bus/Handlers/Commands/Incident/ReportMaintenanceCommandHandler.php b/app/Bus/Handlers/Commands/Incident/ReportMaintenanceCommandHandler.php deleted file mode 100644 index b63b767ac4f8..000000000000 --- a/app/Bus/Handlers/Commands/Incident/ReportMaintenanceCommandHandler.php +++ /dev/null @@ -1,66 +0,0 @@ -dates = $dates; - } - - /** - * Handle the report maintenance command. - * - * @param \CachetHQ\Cachet\Bus\Commands\Incident\ReportMaintenanceCommand $command - * - * @return \CachetHQ\Cachet\Models\Incident - */ - public function handle(ReportMaintenanceCommand $command) - { - $scheduledAt = $this->dates->create('d/m/Y H:i', $command->timestamp); - - $maintenanceEvent = Incident::create([ - 'name' => $command->name, - 'message' => $command->message, - 'scheduled_at' => $scheduledAt, - 'status' => 0, - 'visible' => 1, - 'stickied' => false, - ]); - - $maintenanceEvent->notify = (bool) $command->notify; - - event(new MaintenanceWasScheduledEvent($maintenanceEvent)); - - return $maintenanceEvent; - } -} diff --git a/app/Bus/Handlers/Commands/Schedule/CreateScheduleCommandHandler.php b/app/Bus/Handlers/Commands/Schedule/CreateScheduleCommandHandler.php new file mode 100644 index 000000000000..0f684d277eea --- /dev/null +++ b/app/Bus/Handlers/Commands/Schedule/CreateScheduleCommandHandler.php @@ -0,0 +1,90 @@ + + */ +class CreateScheduleCommandHandler +{ + /** + * The date factory instance. + * + * @var \CachetHQ\Cachet\Dates\DateFactory + */ + protected $dates; + + /** + * Create a new update schedule command handler instance. + * + * @param \CachetHQ\Cachet\Dates\DateFactory $dates + * + * @return void + */ + public function __construct(DateFactory $dates) + { + $this->dates = $dates; + } + + /** + * Handle the create schedule command. + * + * @param \CachetHQ\Cachet\Bus\Commands\Schedule\CreateScheduleCommand $command + * + * @return \CachetHQ\Cachet\Models\Schedule + */ + public function handle(CreateScheduleCommand $command) + { + $schedule = Schedule::create($this->filter($command)); + + event(new ScheduleWasCreatedEvent($schedule)); + + return $schedule; + } + + /** + * Filter the command data. + * + * @param \CachetHQ\Cachet\Bus\Commands\Schedule\CreateScheduleCommand $command + * + * @return array + */ + protected function filter(CreateScheduleCommand $command) + { + $scheduledAt = $this->dates->create('Y-m-d H:i', $command->scheduled_at); + + if ($completedAt = $command->completed_at) { + $completedAt = $this->dates->create('Y-m-d H:i', $command->completed_at); + } + + $params = [ + 'name' => $command->name, + 'message' => $command->message, + 'status' => $command->status, + 'scheduled_at' => $scheduledAt, + 'completed_at' => $completedAt, + ]; + + $availableParams = array_filter($params, function ($val) { + return $val !== null; + }); + + return $availableParams; + } +} diff --git a/app/Bus/Handlers/Commands/Schedule/DeleteScheduleCommandHandler.php b/app/Bus/Handlers/Commands/Schedule/DeleteScheduleCommandHandler.php new file mode 100644 index 000000000000..0ceb5c064123 --- /dev/null +++ b/app/Bus/Handlers/Commands/Schedule/DeleteScheduleCommandHandler.php @@ -0,0 +1,39 @@ + + */ +class DeleteScheduleCommandHandler +{ + /** + * Handle the delete schedule command. + * + * @param \CachetHQ\Cachet\Bus\Commands\Schedule\DeleteScheduleCommand $command + * + * @return void + */ + public function handle(DeleteScheduleCommand $command) + { + $schedule = $command->schedule; + + event(new ScheduleWasRemovedEvent($schedule)); + + $schedule->delete(); + } +} diff --git a/app/Bus/Handlers/Commands/Schedule/UpdateScheduleCommandHandler.php b/app/Bus/Handlers/Commands/Schedule/UpdateScheduleCommandHandler.php new file mode 100644 index 000000000000..e5fb44bf8cb7 --- /dev/null +++ b/app/Bus/Handlers/Commands/Schedule/UpdateScheduleCommandHandler.php @@ -0,0 +1,92 @@ + + */ +class UpdateScheduleCommandHandler +{ + /** + * The date factory instance. + * + * @var \CachetHQ\Cachet\Dates\DateFactory + */ + protected $dates; + + /** + * Create a new update schedule command handler instance. + * + * @param \CachetHQ\Cachet\Dates\DateFactory $dates + * + * @return void + */ + public function __construct(DateFactory $dates) + { + $this->dates = $dates; + } + + /** + * Handle the update schedule command. + * + * @param \CachetHQ\Cachet\Bus\Commands\Schedule\UpdateScheduleCommand $command + * + * @return \CachetHQ\Cachet\Models\Schedule + */ + public function handle(UpdateScheduleCommand $command) + { + $schedule = $command->schedule; + + $schedule->update($this->filter($command)); + + event(new ScheduleWasUpdatedEvent($schedule)); + + return $schedule; + } + + /** + * Filter the command data. + * + * @param \CachetHQ\Cachet\Bus\Commands\Schedule\UpdateScheduleCommand $command + * + * @return array + */ + protected function filter(UpdateScheduleCommand $command) + { + $params = [ + 'name' => $command->name, + 'message' => $command->message, + 'status' => $command->status, + ]; + + if ($scheduleddAt = $command->scheduled_at) { + $params['scheduled_at'] = $this->dates->create('Y-m-d H:i', $scheduledAt); + } + + if ($completedAt = $command->completed_at) { + $params['completed_at'] = $this->dates->create('Y-m-d H:i', $completedAt); + } + + $availableParams = array_filter($params, function ($val) { + return $val !== null; + }); + + return $availableParams; + } +} diff --git a/app/Bus/Handlers/Events/Incident/SendMaintenanceEmailNotificationHandler.php b/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php similarity index 81% rename from app/Bus/Handlers/Events/Incident/SendMaintenanceEmailNotificationHandler.php rename to app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php index 8c922ea59427..6929934b2cb3 100644 --- a/app/Bus/Handlers/Events/Incident/SendMaintenanceEmailNotificationHandler.php +++ b/app/Bus/Handlers/Events/Schedule/SendScheduleEmailNotificationHandler.php @@ -9,15 +9,20 @@ * file that was distributed with this source code. */ -namespace CachetHQ\Cachet\Bus\Handlers\Events\Incident; +namespace CachetHQ\Cachet\Bus\Handlers\Events\Schedule; -use CachetHQ\Cachet\Bus\Events\Incident\MaintenanceWasScheduledEvent; +use CachetHQ\Cachet\Bus\Events\Schedule\ScheduleEventInterface; use CachetHQ\Cachet\Models\Subscriber; use Illuminate\Contracts\Mail\MailQueue; use Illuminate\Mail\Message; use McCool\LaravelAutoPresenter\Facades\AutoPresenter; -class SendMaintenanceEmailNotificationHandler +/** + * This is the send schedule event notification handler. + * + * @author James Brooks + */ +class SendScheduleEmailNotificationHandler { /** * The mailer instance. @@ -50,21 +55,12 @@ public function __construct(MailQueue $mailer, Subscriber $subscriber) /** * Handle the event. * - * @param \CachetHQ\Cachet\Bus\Events\MaintenanceWasScheduledEvent $event + * @param \CachetHQ\Cachet\Bus\Events\Schedule\ScheduleEventInterface $event * * @return void */ - public function handle(MaintenanceWasScheduledEvent $event) + public function handle(ScheduleEventInterface $event) { - if (!$event->incident->notify) { - return false; - } - - // Only send emails for public incidents. - if ($event->incident->visible === 0) { - return; - } - // First notify all global subscribers. $globalSubscribers = $this->subscriber->isVerified()->isGlobal()->get(); @@ -72,10 +68,6 @@ public function handle(MaintenanceWasScheduledEvent $event) $this->notify($event, $subscriber); } - if (!$event->incident->component) { - return; - } - $notified = $globalSubscribers->pluck('id')->all(); // Notify the remaining component specific subscribers. @@ -95,12 +87,12 @@ public function handle(MaintenanceWasScheduledEvent $event) /** * Send notification to subscriber. * - * @param \CachetHQ\Cachet\Bus\Events\MaintenanceWasScheduledEvent $event - * @param \CachetHQ\Cachet\Models\Subscriber $subscriber + * @param \CachetHQ\Cachet\Bus\Events\Schedule\ScheduleEventInterface $event + * @param \CachetHQ\Cachet\Models\Subscriber $subscriber * * @return \Illuminate\Database\Eloquent\Collection */ - public function notify(MaintenanceWasScheduledEvent $event, $subscriber) + public function notify(ScheduleEventInterface $event, $subscriber) { $incident = AutoPresenter::decorate($event->incident); $component = AutoPresenter::decorate($event->incident->component); diff --git a/app/Composers/DashboardComposer.php b/app/Composers/DashboardComposer.php index e30145f04343..e69a54f8b57e 100644 --- a/app/Composers/DashboardComposer.php +++ b/app/Composers/DashboardComposer.php @@ -14,6 +14,7 @@ use CachetHQ\Cachet\Models\Component; use CachetHQ\Cachet\Models\Incident; use CachetHQ\Cachet\Models\IncidentTemplate; +use CachetHQ\Cachet\Models\Schedule; use CachetHQ\Cachet\Models\Subscriber; use Illuminate\Contracts\View\View; @@ -34,9 +35,10 @@ class DashboardComposer */ public function compose(View $view) { - $view->withIncidentCount(Incident::notScheduled()->count()); + $view->withComponentCount(Component::count()); + $view->withIncidentCount(Incident::count()); $view->withIncidentTemplateCount(IncidentTemplate::count()); - $view->withComponentCount(Component::all()->count()); + $view->withScheduleCount(Schedule::count()); $view->withSubscriberCount(Subscriber::isVerified()->count()); } } diff --git a/app/Composers/Modules/ScheduledComposer.php b/app/Composers/Modules/ScheduledComposer.php index d2f3a927221a..642984f5d2f1 100644 --- a/app/Composers/Modules/ScheduledComposer.php +++ b/app/Composers/Modules/ScheduledComposer.php @@ -11,7 +11,7 @@ namespace CachetHQ\Cachet\Composers\Modules; -use CachetHQ\Cachet\Models\Incident; +use CachetHQ\Cachet\Models\Schedule; use Illuminate\Contracts\View\View; /** @@ -31,7 +31,7 @@ class ScheduledComposer */ public function compose(View $view) { - $scheduledMaintenance = Incident::scheduled()->orderBy('scheduled_at')->get(); + $scheduledMaintenance = Schedule::futureSchedules()->orderBy('scheduled_at')->get(); $view->withScheduledMaintenance($scheduledMaintenance); } diff --git a/app/Composers/Modules/StickiedComposer.php b/app/Composers/Modules/StickiedComposer.php index bd599c72fe38..948abbcc5060 100644 --- a/app/Composers/Modules/StickiedComposer.php +++ b/app/Composers/Modules/StickiedComposer.php @@ -33,9 +33,10 @@ class StickiedComposer */ public function compose(View $view) { - $stickiedIncidents = Incident::stickied()->orderBy('scheduled_at', 'desc')->orderBy('occurred_at', 'desc')->get()->groupBy(function (Incident $incident) { + $stickiedIncidents = Incident::stickied()->orderBy('occurred_at', 'desc')->get()->groupBy(function (Incident $incident) { return app(DateFactory::class)->make($incident->is_scheduled ? $incident->scheduled_at : $incident->occurred_at)->toDateString(); }); + $view->withStickiedIncidents($stickiedIncidents); } } diff --git a/app/Console/Commands/DemoSeederCommand.php b/app/Console/Commands/DemoSeederCommand.php index f5581d8c94de..3f8d6a1e77c5 100644 --- a/app/Console/Commands/DemoSeederCommand.php +++ b/app/Console/Commands/DemoSeederCommand.php @@ -18,6 +18,7 @@ use CachetHQ\Cachet\Models\IncidentUpdate; use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\MetricPoint; +use CachetHQ\Cachet\Models\Schedule; use CachetHQ\Cachet\Models\Subscriber; use CachetHQ\Cachet\Models\User; use CachetHQ\Cachet\Settings\Repository; @@ -89,6 +90,7 @@ public function fire() $this->seedIncidentTemplates(); $this->seedMetricPoints(); $this->seedMetrics(); + $this->seedSchedules(); $this->seedSettings(); $this->seedSubscribers(); $this->seedUsers(); @@ -207,7 +209,6 @@ protected function seedIncidents() 'message' => 'We\'re investigating an issue with our monkeys not performing as they should be.', 'status' => Incident::INVESTIGATING, 'component_id' => 0, - 'scheduled_at' => null, 'visible' => 1, 'stickied' => false, 'occurred_at' => Carbon::now(), @@ -217,7 +218,6 @@ protected function seedIncidents() 'message' => 'Unresolved incidents are left without a **Fixed** update.', 'status' => Incident::INVESTIGATING, 'component_id' => 0, - 'scheduled_at' => null, 'visible' => 1, 'stickied' => false, 'occurred_at' => Carbon::now(), @@ -332,6 +332,27 @@ protected function seedMetrics() } } + /** + * Seed the schedules table. + * + * @return void + */ + protected function seedSchedules() + { + $defaultSchedules = [ + [ + 'name' => 'Demo resets every half hour!', + 'message' => 'You can schedule downtime for _your_ service!', + 'status' => Schedule::UPCOMING, + 'scheduled_at' => (new DateTime())->add(new DateInterval('PT2H')), + ], + ]; + + foreach ($defaultSchedules as $schedule) { + Schedule::create($schedule); + } + } + /** * Seed the settings table. * diff --git a/app/Foundation/Providers/EventServiceProvider.php b/app/Foundation/Providers/EventServiceProvider.php index 0bdee120e895..626379c32b6b 100644 --- a/app/Foundation/Providers/EventServiceProvider.php +++ b/app/Foundation/Providers/EventServiceProvider.php @@ -69,9 +69,6 @@ class EventServiceProvider extends ServiceProvider 'CachetHQ\Cachet\Bus\Events\Incident\IncidentWasRemovedEvent' => [ // ], - 'CachetHQ\Cachet\Bus\Events\Incident\MaintenanceWasScheduledEvent' => [ - 'CachetHQ\Cachet\Bus\Handlers\Events\Incident\SendMaintenanceEmailNotificationHandler', - ], 'CachetHQ\Cachet\Bus\Events\Invite\InviteWasClaimedEvent' => [ // ], @@ -93,6 +90,15 @@ class EventServiceProvider extends ServiceProvider 'CachetHQ\Cachet\Bus\Events\Metric\MetricWasUpdatedEvent' => [ // ], + 'CachetHQ\Cachet\Bus\Events\Schedule\ScheduleWasCreatedEvent' => [ + // 'CachetHQ\Cachet\Bus\Handlers\Events\Schedule\SendScheduleEmailNotificationHandler', + ], + 'CachetHQ\Cachet\Bus\Events\Schedule\ScheduleWasRemovedEvent' => [ + // + ], + 'CachetHQ\Cachet\Bus\Events\Schedule\ScheduleWasUpdatedEvent' => [ + // + ], 'CachetHQ\Cachet\Bus\Events\Subscriber\SubscriberHasSubscribedEvent' => [ 'CachetHQ\Cachet\Bus\Handlers\Events\Subscriber\SendSubscriberVerificationEmailHandler', ], diff --git a/app/Foundation/Providers/RouteServiceProvider.php b/app/Foundation/Providers/RouteServiceProvider.php index c78f873f4db5..d0982587bf25 100644 --- a/app/Foundation/Providers/RouteServiceProvider.php +++ b/app/Foundation/Providers/RouteServiceProvider.php @@ -69,6 +69,7 @@ public function bind(Router $router) $router->model('incident_update', 'CachetHQ\Cachet\Models\IncidentUpdate'); $router->model('metric', 'CachetHQ\Cachet\Models\Metric'); $router->model('metric_point', 'CachetHQ\Cachet\Models\MetricPoint'); + $router->model('schedule', 'CachetHQ\Cachet\Models\Schedule'); $router->model('setting', 'CachetHQ\Cachet\Models\Setting'); $router->model('subscriber', 'CachetHQ\Cachet\Models\Subscriber'); $router->model('subscription', 'CachetHQ\Cachet\Models\Subscription'); diff --git a/app/Http/Controllers/Api/ScheduleController.php b/app/Http/Controllers/Api/ScheduleController.php new file mode 100644 index 000000000000..b3c9f73a7756 --- /dev/null +++ b/app/Http/Controllers/Api/ScheduleController.php @@ -0,0 +1,128 @@ + + */ +class ScheduleController extends AbstractApiController +{ + /** + * Return all schedules. + * + * @return \Illuminate\Http\JsonResponse + */ + public function getSchedules() + { + $schedule = Schedule::whereRaw('1 = 1'); + + if ($sortBy = Binput::get('sort')) { + $direction = Binput::has('order') && Binput::get('order') == 'desc'; + + $schedule->sort($sortBy, $direction); + } + + $schedule = $schedule->paginate(Binput::get('per_page', 20)); + + return $this->paginator($schedule, Request::instance()); + } + + /** + * Return a single schedule. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return \Illuminate\Http\JsonResponse + */ + public function getSchedule(Schedule $schedule) + { + return $this->item($schedule); + } + + /** + * Create a new schedule. + * + * @return \Illuminate\Http\JsonResponse + */ + public function postSchedule() + { + try { + $schedule = dispatch(new CreateScheduleCommand( + Binput::get('name'), + Binput::get('message'), + Binput::get('status'), + Binput::get('scheduled_at'), + Binput::get('completed_at'), + Binput::get('components', []) + )); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->item($schedule); + } + + /** + * Update a schedule. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return \Illuminate\Http\JsonResponse + */ + public function putSchedule(Schedule $schedule) + { + try { + $schedule = dispatch(new UpdateScheduleCommand( + $schedule, + Binput::get('name'), + Binput::get('message'), + Binput::get('status'), + Binput::get('scheduled_at'), + Binput::get('completed_at'), + Binput::get('components', []) + )); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->item($schedule); + } + + /** + * Delete a schedule. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return \Illuminate\Http\JsonResponse + */ + public function deleteSchedule(Schedule $schedule) + { + try { + dispatch(new DeleteScheduleCommand($schedule)); + } catch (QueryException $e) { + throw new BadRequestHttpException(); + } + + return $this->noContent(); + } +} diff --git a/app/Http/Controllers/Dashboard/DashboardController.php b/app/Http/Controllers/Dashboard/DashboardController.php index 0597ca3f2fea..13f4d2d9dbf4 100644 --- a/app/Http/Controllers/Dashboard/DashboardController.php +++ b/app/Http/Controllers/Dashboard/DashboardController.php @@ -127,7 +127,7 @@ public function showDashboard() */ protected function getIncidents() { - $allIncidents = Incident::notScheduled()->whereBetween('occurred_at', [ + $allIncidents = Incident::whereBetween('occurred_at', [ $this->startDate->copy()->subDays(30)->format('Y-m-d').' 00:00:00', $this->startDate->format('Y-m-d').' 23:59:59', ])->orderBy('occurred_at', 'desc')->get()->groupBy(function (Incident $incident) { diff --git a/app/Http/Controllers/Dashboard/IncidentController.php b/app/Http/Controllers/Dashboard/IncidentController.php index 8e5bf34ac7bc..0c063cc1497d 100644 --- a/app/Http/Controllers/Dashboard/IncidentController.php +++ b/app/Http/Controllers/Dashboard/IncidentController.php @@ -57,22 +57,6 @@ public function __construct(Guard $auth) { $this->auth = $auth; - $this->subMenu = [ - 'incidents' => [ - 'title' => trans('dashboard.incidents.incidents'), - 'url' => cachet_route('dashboard.incidents'), - 'icon' => 'ion-android-checkmark-circle', - 'active' => true, - ], - 'schedule' => [ - 'title' => trans('dashboard.schedule.schedule'), - 'url' => cachet_route('dashboard.schedule'), - 'icon' => 'ion-android-calendar', - 'active' => false, - ], - ]; - - View::share('sub_menu', $this->subMenu); View::share('sub_title', trans('dashboard.incidents.title')); } @@ -83,7 +67,7 @@ public function __construct(Guard $auth) */ public function showIncidents() { - $incidents = Incident::notScheduled()->orderBy('created_at', 'desc')->get(); + $incidents = Incident::orderBy('created_at', 'desc')->get(); return View::make('dashboard.incidents.index') ->withPageTitle(trans('dashboard.incidents.incidents').' - '.trans('dashboard.dashboard')) diff --git a/app/Http/Controllers/Dashboard/ScheduleController.php b/app/Http/Controllers/Dashboard/ScheduleController.php index d65bca169578..cdf50c9fd926 100644 --- a/app/Http/Controllers/Dashboard/ScheduleController.php +++ b/app/Http/Controllers/Dashboard/ScheduleController.php @@ -12,16 +12,20 @@ namespace CachetHQ\Cachet\Http\Controllers\Dashboard; use AltThree\Validator\ValidationException; -use CachetHQ\Cachet\Bus\Commands\Incident\ReportMaintenanceCommand; -use CachetHQ\Cachet\Dates\DateFactory; -use CachetHQ\Cachet\Models\Incident; +use CachetHQ\Cachet\Bus\Commands\Schedule\CreateScheduleCommand; +use CachetHQ\Cachet\Bus\Commands\Schedule\DeleteScheduleCommand; +use CachetHQ\Cachet\Bus\Commands\Schedule\UpdateScheduleCommand; use CachetHQ\Cachet\Models\IncidentTemplate; +use CachetHQ\Cachet\Models\Schedule; use GrahamCampbell\Binput\Facades\Binput; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\View; -use Illuminate\Support\MessageBag; -use Jenssegers\Date\Date; +/** + * This is the schedule controller class. + * + * @author James Brooks + */ class ScheduleController extends Controller { /** @@ -38,23 +42,7 @@ class ScheduleController extends Controller */ public function __construct() { - $this->subMenu = [ - 'incidents' => [ - 'title' => trans('dashboard.incidents.incidents'), - 'url' => cachet_route('dashboard.incidents'), - 'icon' => 'ion-android-checkmark-circle', - 'active' => false, - ], - 'schedule' => [ - 'title' => trans('dashboard.schedule.schedule'), - 'url' => cachet_route('dashboard.schedule'), - 'icon' => 'ion-android-calendar', - 'active' => true, - ], - ]; - - View::share('sub_menu', $this->subMenu); - View::share('sub_title', trans('dashboard.incidents.title')); + View::share('sub_title', trans('dashboard.schedule.title')); } /** @@ -64,7 +52,7 @@ public function __construct() */ public function showIndex() { - $schedule = Incident::scheduled()->orderBy('created_at')->get(); + $schedule = Schedule::orderBy('created_at')->get(); return View::make('dashboard.schedule.index') ->withPageTitle(trans('dashboard.schedule.schedule').' - '.trans('dashboard.dashboard')) @@ -86,18 +74,20 @@ public function showAddSchedule() } /** - * Creates a new scheduled maintenance "incident". + * Creates a new scheduled maintenance. * * @return \Illuminate\Http\RedirectResponse */ public function addScheduleAction() { try { - $incident = dispatch(new ReportMaintenanceCommand( + dispatch(new CreateScheduleCommand( Binput::get('name'), Binput::get('message'), - Binput::get('notify'), - Binput::get('scheduled_at') + Binput::get('status', Schedule::UPCOMING), + Binput::get('scheduled_at'), + Binput::get('completed_at'), + Binput::get('components', []) )); } catch (ValidationException $e) { return cachet_redirect('dashboard.schedule.create') @@ -113,11 +103,11 @@ public function addScheduleAction() /** * Shows the edit schedule maintenance form. * - * @param \CachetHQ\Cachet\Models\Incident $schedule + * @param \CachetHQ\Cachet\Models\Schedule $schedule * * @return \Illuminate\View\View */ - public function showEditSchedule(Incident $schedule) + public function showEditSchedule(Schedule $schedule) { $incidentTemplates = IncidentTemplate::all(); @@ -130,30 +120,22 @@ public function showEditSchedule(Incident $schedule) /** * Updates the given incident. * - * @param \CachetHQ\Cachet\Models\Incident $schedule + * @param \CachetHQ\Cachet\Models\Schedule $schedule * * @return \Illuminate\Http\RedirectResponse */ - public function editScheduleAction(Incident $schedule) + public function editScheduleAction(Schedule $schedule) { - $scheduleData = Binput::get('incident'); - - // Parse the schedule date. - $scheduledAt = app(DateFactory::class)->create('d/m/Y H:i', $scheduleData['scheduled_at']); - - if ($scheduledAt->isPast()) { - $messageBag = new MessageBag(); - $messageBag->add('scheduled_at', trans('validation.date', ['attribute' => 'scheduled time you supplied'])); - - return cachet_redirect('dashboard.schedule.edit', [$schedule->id])->withErrors($messageBag); - } - - $scheduleData['scheduled_at'] = $scheduledAt; - // Bypass the incident.status field. - $scheduleData['status'] = 0; - try { - $schedule->update($scheduleData); + $schedule = dispatch(new UpdateScheduleCommand( + $schedule, + Binput::get('name', null), + Binput::get('message', null), + Binput::get('status', null), + Binput::get('scheduled_at', null), + Binput::get('completed_at', null), + Binput::get('components', []) + )); } catch (ValidationException $e) { return cachet_redirect('dashboard.schedule.edit', [$schedule->id]) ->withInput(Binput::all()) @@ -168,13 +150,13 @@ public function editScheduleAction(Incident $schedule) /** * Deletes a given schedule. * - * @param \CachetHQ\Cachet\Models\Incident $schedule + * @param \CachetHQ\Cachet\Models\Schedule $schedule * * @return \Illuminate\Http\RedirectResponse */ - public function deleteScheduleAction(Incident $schedule) + public function deleteScheduleAction(Schedule $schedule) { - $schedule->delete(); + dispatch(new DeleteScheduleCommand($schedule)); return cachet_redirect('dashboard.schedule') ->withSuccess(sprintf('%s %s', trans('dashboard.notifications.awesome'), trans('dashboard.schedule.delete.success'))); diff --git a/app/Http/Controllers/StatusPageController.php b/app/Http/Controllers/StatusPageController.php index bf257498ccdf..982ed6c961a6 100644 --- a/app/Http/Controllers/StatusPageController.php +++ b/app/Http/Controllers/StatusPageController.php @@ -17,6 +17,7 @@ use CachetHQ\Cachet\Models\Component; use CachetHQ\Cachet\Models\Incident; use CachetHQ\Cachet\Models\Metric; +use CachetHQ\Cachet\Models\Schedule; use CachetHQ\Cachet\Repositories\Metric\MetricRepository; use Exception; use GrahamCampbell\Binput\Facades\Binput; @@ -84,11 +85,11 @@ public function showIndex() $incidentVisibility = Auth::check() ? 0 : 1; - $allIncidents = Incident::notScheduled()->where('visible', '>=', $incidentVisibility)->whereBetween('occurred_at', [ + $allIncidents = Incident::where('visible', '>=', $incidentVisibility)->whereBetween('occurred_at', [ $startDate->copy()->subDays($daysToShow)->format('Y-m-d').' 00:00:00', $startDate->format('Y-m-d').' 23:59:59', - ])->orderBy('scheduled_at', 'desc')->orderBy('occurred_at', 'desc')->get()->groupBy(function (Incident $incident) { - return app(DateFactory::class)->make($incident->is_scheduled ? $incident->scheduled_at : $incident->occurred_at)->toDateString(); + ])->orderBy('occurred_at', 'desc')->get()->groupBy(function (Incident $incident) { + return app(DateFactory::class)->make($incident->occurred_at)->toDateString(); }); // Add in days that have no incidents @@ -111,7 +112,7 @@ public function showIndex() ->withDaysToShow($daysToShow) ->withAllIncidents($allIncidents) ->withCanPageForward((bool) $today->gt($startDate)) - ->withCanPageBackward(Incident::notScheduled()->where('occurred_at', '<', $startDate->format('Y-m-d'))->count() > 0) + ->withCanPageBackward(Incident::where('occurred_at', '<', $startDate->format('Y-m-d'))->count() > 0) ->withPreviousDate($startDate->copy()->subDays($daysToShow)->toDateString()) ->withNextDate($startDate->copy()->addDays($daysToShow)->toDateString()); } @@ -125,8 +126,19 @@ public function showIndex() */ public function showIncident(Incident $incident) { - return View::make('single-incident') - ->withIncident($incident); + return View::make('single-incident')->withIncident($incident); + } + + /** + * Show a single schedule. + * + * @param \CachetHQ\Cachet\Models\Schedule $schedule + * + * @return \Illuminate\View\View + */ + public function showSchedule(Schedule $schedule) + { + return View::make('single-schedule')->withSchedule($schedule); } /** diff --git a/app/Http/Routes/ApiRoutes.php b/app/Http/Routes/ApiRoutes.php index f255e317fd3f..9ec12e58015e 100644 --- a/app/Http/Routes/ApiRoutes.php +++ b/app/Http/Routes/ApiRoutes.php @@ -59,6 +59,9 @@ public function map(Registrar $router) $router->get('metrics', 'MetricController@getMetrics'); $router->get('metrics/{metric}', 'MetricController@getMetric'); $router->get('metrics/{metric}/points', 'MetricController@getMetricPoints'); + + $router->get('schedules', 'ScheduleController@getSchedules'); + $router->get('schedules/{schedule}', 'ScheduleController@getSchedule'); }); $router->group(['middleware' => ['auth.api:true']], function (Registrar $router) { @@ -70,6 +73,7 @@ public function map(Registrar $router) $router->post('incidents/{incident}/updates', 'IncidentUpdateController@postIncidentUpdate'); $router->post('metrics', 'MetricController@postMetrics'); $router->post('metrics/{metric}/points', 'MetricPointController@postMetricPoints'); + $router->post('schedules', 'ScheduleController@postSchedule'); $router->post('subscribers', 'SubscriberController@postSubscribers'); $router->put('components/groups/{component_group}', 'ComponentGroupController@putGroup'); @@ -78,6 +82,7 @@ public function map(Registrar $router) $router->put('incidents/{incident}/updates/{update}', 'IncidentUpdateController@putIncidentUpdate'); $router->put('metrics/{metric}', 'MetricController@putMetric'); $router->put('metrics/{metric}/points/{metric_point}', 'MetricPointController@putMetricPoint'); + $router->put('schedules/{schedule}', 'ScheduleController@putSchedule'); $router->delete('components/groups/{component_group}', 'ComponentGroupController@deleteGroup'); $router->delete('components/{component}', 'ComponentController@deleteComponent'); @@ -85,6 +90,7 @@ public function map(Registrar $router) $router->delete('incidents/{incident}/updates/{update}', 'IncidentUpdateController@deleteIncidentUpdate'); $router->delete('metrics/{metric}', 'MetricController@deleteMetric'); $router->delete('metrics/{metric}/points/{metric_point}', 'MetricPointController@deleteMetricPoint'); + $router->delete('schedules/{schedule}', 'ScheduleController@deleteSchedule'); $router->delete('subscribers/{subscriber}', 'SubscriberController@deleteSubscriber'); $router->delete('subscriptions/{subscription}', 'SubscriberController@deleteSubscription'); }); diff --git a/app/Http/Routes/Dashboard/ScheduleRoutes.php b/app/Http/Routes/Dashboard/ScheduleRoutes.php index 506fe51b6d75..8591d1500c95 100644 --- a/app/Http/Routes/Dashboard/ScheduleRoutes.php +++ b/app/Http/Routes/Dashboard/ScheduleRoutes.php @@ -56,15 +56,15 @@ public function map(Registrar $router) 'uses' => 'ScheduleController@addScheduleAction', ]); - $router->get('{incident}', [ + $router->get('{schedule}', [ 'as' => 'get:dashboard.schedule.edit', 'uses' => 'ScheduleController@showEditSchedule', ]); - $router->post('{incident}', [ + $router->post('{schedule}', [ 'as' => 'post:dashboard.schedule.edit', 'uses' => 'ScheduleController@editScheduleAction', ]); - $router->delete('{incident}', [ + $router->delete('{schedule}', [ 'as' => 'delete:dashboard.schedule.delete', 'uses' => 'ScheduleController@deleteScheduleAction', ]); diff --git a/app/Http/Routes/StatusPageRoutes.php b/app/Http/Routes/StatusPageRoutes.php index 05df54e7db7d..20bd69089af8 100644 --- a/app/Http/Routes/StatusPageRoutes.php +++ b/app/Http/Routes/StatusPageRoutes.php @@ -49,6 +49,11 @@ public function map(Registrar $router) 'uses' => 'StatusPageController@showIncident', ]); + $router->get('schedules/{schedule}', [ + 'as' => 'get:schedule', + 'uses' => 'StatusPageController@showSchedule', + ]); + $router->get('metrics/{metric}', [ 'as' => 'get:metric', 'uses' => 'StatusPageController@getMetrics', diff --git a/app/Integrations/Core/System.php b/app/Integrations/Core/System.php index 0b83a8e6c8a2..ae5fb29373f4 100644 --- a/app/Integrations/Core/System.php +++ b/app/Integrations/Core/System.php @@ -49,7 +49,7 @@ public function getStatus() ]; } elseif ($enabledScope->notStatus(1)->count() === 0) { // If all our components are ok, do we have any non-fixed incidents? - $incidents = Incident::notScheduled()->orderBy('occurred_at', 'desc')->get()->filter(function ($incident) { + $incidents = Incident::orderBy('occurred_at', 'desc')->get()->filter(function ($incident) { return $incident->status > 0; }); $incidentCount = $incidents->count(); diff --git a/app/Models/Incident.php b/app/Models/Incident.php index ba396b8cf619..8ab5e62fa53e 100644 --- a/app/Models/Incident.php +++ b/app/Models/Incident.php @@ -15,7 +15,6 @@ use CachetHQ\Cachet\Models\Traits\SearchableTrait; use CachetHQ\Cachet\Models\Traits\SortableTrait; use CachetHQ\Cachet\Presenters\IncidentPresenter; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -75,11 +74,10 @@ class Incident extends Model implements HasPresenter * @var string[] */ protected $casts = [ - 'visible' => 'int', - 'stickied' => 'bool', - 'scheduled_at' => 'date', - 'occurred_at' => 'date', - 'deleted_at' => 'date', + 'visible' => 'int', + 'stickied' => 'bool', + 'occurred_at' => 'date', + 'deleted_at' => 'date', ]; /** @@ -94,7 +92,6 @@ class Incident extends Model implements HasPresenter 'visible', 'stickied', 'message', - 'scheduled_at', 'occurred_at', 'created_at', 'updated_at', @@ -194,44 +191,6 @@ public function scopeStickied(Builder $query) return $query->where('stickied', '=', true); } - /** - * Finds all scheduled incidents (maintenance). - * - * @param \Illuminate\Database\Eloquent\Builder $query - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeScheduled(Builder $query) - { - return $query->where('status', '=', 0)->where('scheduled_at', '>=', Carbon::now()); - } - - /** - * Finds all non-scheduled incidents. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function scopeNotScheduled(Builder $query) - { - return $query->where('status', '>', 0)->orWhere(function ($query) { - $query->where('status', '=', 0)->where(function ($query) { - $query->whereNull('scheduled_at')->orWhere('scheduled_at', '<=', Carbon::now()); - }); - }); - } - - /** - * Returns whether the "incident" is scheduled or not. - * - * @return bool - */ - public function getIsScheduledAttribute() - { - return $this->getOriginal('scheduled_at') !== null; - } - /** * Is the incident resolved? * diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php new file mode 100644 index 000000000000..47b6d56a4862 --- /dev/null +++ b/app/Models/Schedule.php @@ -0,0 +1,162 @@ + 'string', + 'message' => 'string', + 'status' => 'int', + 'scheduled_at' => 'date', + 'completed_at' => 'date', + ]; + + /** + * The fillable properties. + * + * @var string[] + */ + protected $fillable = [ + 'name', + 'message', + 'status', + 'scheduled_at', + 'completed_at', + 'created_at', + 'updated_at', + ]; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'name' => 'required|string', + 'message' => 'nullable|string', + 'status' => 'required|int|between:0,2', + ]; + + /** + * The searchable fields. + * + * @var string[] + */ + protected $searchable = [ + 'id', + 'name', + 'status', + ]; + + /** + * The sortable fields. + * + * @var string[] + */ + protected $sortable = [ + 'id', + 'name', + 'status', + 'scheduled_at', + 'completed_at', + 'created_at', + 'updated_at', + ]; + + /** + * The relations to eager load on every query. + * + * @var string[] + */ + protected $with = ['components']; + + /** + * Scopes schedules to those in the future. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeFutureSchedules($query) + { + return $query->whereIn('status', [self::UPCOMING, self::IN_PROGRESS])->where('scheduled_at', '>=', Carbon::now()); + } + + /** + * Scopes schedules to those in the past. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopePastSchedules($query) + { + return $query->where('status', '<', self::COMPLETE)->where('scheduled_at', '<=', Carbon::now()); + } + + /** + * Get the components relation. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function components() + { + return $this->hasMany(ScheduleComponent::class); + } + + /** + * Get the presenter class. + * + * @return string + */ + public function getPresenterClass() + { + return SchedulePresenter::class; + } +} diff --git a/app/Models/ScheduleComponent.php b/app/Models/ScheduleComponent.php new file mode 100644 index 000000000000..a77abcd27810 --- /dev/null +++ b/app/Models/ScheduleComponent.php @@ -0,0 +1,73 @@ + 'int', + 'component_id' => 'int', + 'component_status' => 'int', + ]; + + /** + * The fillable properties. + * + * @var string[] + */ + protected $fillable = [ + 'schedule_id', + 'component_id', + 'component_status', + ]; + + /** + * The validation rules. + * + * @var string[] + */ + public $rules = [ + 'schedule_id' => 'required|int', + 'component_id' => 'required|int', + 'component_status' => 'required|int', + ]; + + /** + * Get the schedule relation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function schedule() + { + return $this->belongsTo(Schedule::class); + } + + /** + * Get the component relation. + * + * @return \Illuminate\Database\Eloquent\Relations\HasOne + */ + public function component() + { + return $this->hasOne(Component::class); + } +} diff --git a/app/Presenters/IncidentPresenter.php b/app/Presenters/IncidentPresenter.php index 17e65a825c3f..cb790c62a7ab 100644 --- a/app/Presenters/IncidentPresenter.php +++ b/app/Presenters/IncidentPresenter.php @@ -157,56 +157,6 @@ public function created_at_iso() return $this->dates->make($this->wrappedObject->created_at)->toISO8601String(); } - /** - * Present formatted date time. - * - * @return string - */ - public function scheduled_at() - { - return $this->dates->make($this->wrappedObject->scheduled_at)->toDateTimeString(); - } - - /** - * Present diff for humans date time. - * - * @return string - */ - public function scheduled_at_diff() - { - return $this->dates->make($this->wrappedObject->scheduled_at)->diffForHumans(); - } - - /** - * Present formatted date time. - * - * @return string - */ - public function scheduled_at_formatted() - { - return ucfirst($this->dates->make($this->wrappedObject->scheduled_at)->format($this->incidentDateFormat())); - } - - /** - * Present formatted date time. - * - * @return string - */ - public function scheduled_at_iso() - { - return $this->dates->make($this->wrappedObject->scheduled_at)->toISO8601String(); - } - - /** - * Formats the scheduled_at time ready to be used by bootstrap-datetimepicker. - * - * @return string - */ - public function scheduled_at_datetimepicker() - { - return $this->dates->make($this->wrappedObject->scheduled_at)->format('d/m/Y H:i'); - } - /** * Returns a formatted timestamp for use within the timeline. * @@ -214,10 +164,6 @@ public function scheduled_at_datetimepicker() */ public function timestamp_formatted() { - if ($this->wrappedObject->is_scheduled) { - return $this->scheduled_at_formatted; - } - return $this->occurred_at_formatted; } @@ -228,10 +174,6 @@ public function timestamp_formatted() */ public function timestamp_iso() { - if ($this->wrappedObject->is_scheduled) { - return $this->scheduled_at_iso; - } - return $this->occurred_at_iso; } @@ -352,7 +294,6 @@ public function toArray() 'latest_icon' => $this->latest_icon(), 'permalink' => $this->permalink(), 'duration' => $this->duration(), - 'scheduled_at' => $this->scheduled_at(), 'occurred_at' => $this->occurred_at(), 'created_at' => $this->created_at(), 'updated_at' => $this->updated_at(), diff --git a/app/Presenters/SchedulePresenter.php b/app/Presenters/SchedulePresenter.php new file mode 100644 index 000000000000..42c13bbd6ee6 --- /dev/null +++ b/app/Presenters/SchedulePresenter.php @@ -0,0 +1,261 @@ + + */ +class SchedulePresenter extends BasePresenter implements Arrayable +{ + use TimestampsTrait; + + /** + * The date factory instance. + * + * @var \CachetHQ\Cachet\Dates\DateFactory + */ + protected $dates; + + /** + * Create a new presenter. + * + * @param \CachetHQ\Cachet\Dates\DateFactory $dates + * @param \CachetHQ\Cachet\Models\Schedule $resource + * + * @return void + */ + public function __construct(DateFactory $dates, Schedule $resource) + { + $this->dates = $dates; + + parent::__construct($resource); + } + + /** + * Renders the message from Markdown into HTML. + * + * @return string + */ + public function formattedMessage() + { + return Markdown::convertToHtml($this->wrappedObject->message); + } + + /** + * Present diff for humans date time. + * + * @return string + */ + public function created_at_diff() + { + return $this->dates->make($this->wrappedObject->created_at)->diffForHumans(); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function created_at_formatted() + { + return ucfirst($this->dates->make($this->wrappedObject->created_at)->format($this->incidentDateFormat())); + } + + /** + * Formats the created_at time ready to be used by bootstrap-datetimepicker. + * + * @return string + */ + public function created_at_datetimepicker() + { + return $this->dates->make($this->wrappedObject->created_at)->format('Y-m-d H:i'); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function created_at_iso() + { + return $this->dates->make($this->wrappedObject->created_at)->toISO8601String(); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function scheduled_at() + { + return $this->dates->make($this->wrappedObject->scheduled_at)->toDateTimeString(); + } + + /** + * Present diff for humans date time. + * + * @return string + */ + public function scheduled_at_diff() + { + return $this->dates->make($this->wrappedObject->scheduled_at)->diffForHumans(); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function scheduled_at_formatted() + { + return ucfirst($this->dates->make($this->wrappedObject->scheduled_at)->format($this->incidentDateFormat())); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function scheduled_at_iso() + { + return $this->dates->make($this->wrappedObject->scheduled_at)->toISO8601String(); + } + + /** + * Formats the scheduled_at time ready to be used by bootstrap-datetimepicker. + * + * @return string + */ + public function scheduled_at_datetimepicker() + { + return $this->dates->make($this->wrappedObject->scheduled_at)->format('Y-m-d H:i'); + } + + /** + * Returns a formatted timestamp for use within the timeline. + * + * @return string + */ + public function timestamp_formatted() + { + if ($this->wrappedObject->is_scheduled) { + return $this->scheduled_at_formatted; + } + + return $this->created_at_formatted; + } + + /** + * Present formatted date time. + * + * @return string + */ + public function completed_at() + { + return $this->dates->make($this->wrappedObject->completed_at)->toDateTimeString(); + } + + /** + * Present diff for humans date time. + * + * @return string + */ + public function completed_at_diff() + { + return $this->dates->make($this->wrappedObject->completed_at)->diffForHumans(); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function completed_at_formatted() + { + return ucfirst($this->dates->make($this->wrappedObject->completed_at)->format($this->incidentDateFormat())); + } + + /** + * Present formatted date time. + * + * @return string + */ + public function completed_at_iso() + { + return $this->dates->make($this->wrappedObject->completed_at)->toISO8601String(); + } + + /** + * Formats the completed_at time ready to be used by bootstrap-datetimepicker. + * + * @return string + */ + public function completed_at_datetimepicker() + { + return $this->dates->make($this->wrappedObject->completed_at)->format('Y-m-d H:i'); + } + + /** + * Return the iso timestamp for use within the timeline. + * + * @return string + */ + public function timestamp_iso() + { + if ($this->wrappedObject->is_scheduled) { + return $this->scheduled_at_iso; + } + + return $this->completed_at_iso; + } + + /** + * Returns a human readable version of the status. + * + * @return string + */ + public function human_status() + { + // return trans('cachet.incidents.status.'.$this->wrappedObject->status); + // TODO: Refactor into translations. + switch ($this->wrappedObject->status) { + case 0: return 'Upcoming'; + case 1: return 'In Progress'; + case 2: return 'Complete'; + } + } + + /** + * Convert the presenter instance to an array. + * + * @return string[] + */ + public function toArray() + { + return array_merge($this->wrappedObject->toArray(), [ + 'human_status' => $this->human_status(), + 'scheduled_at' => $this->scheduled_at(), + 'completed_at' => $this->completed_at(), + 'created_at' => $this->created_at(), + 'updated_at' => $this->updated_at(), + ]); + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 1df478453e53..e5c33dd727f0 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -16,6 +16,7 @@ use CachetHQ\Cachet\Models\IncidentUpdate; use CachetHQ\Cachet\Models\Metric; use CachetHQ\Cachet\Models\MetricPoint; +use CachetHQ\Cachet\Models\Schedule; use CachetHQ\Cachet\Models\Setting; use CachetHQ\Cachet\Models\Subscriber; use CachetHQ\Cachet\Models\Subscription; @@ -89,6 +90,15 @@ ]; }); +$factory->define(Schedule::class, function ($faker) { + return [ + 'name' => $faker->sentence(), + 'message' => $faker->paragraph(), + 'status' => Schedule::UPCOMING, + 'scheduled_at' => Carbon::now()->addDays(7), + ]; +}); + $factory->define(Setting::class, function ($faker) { return [ 'name' => 'app_name', diff --git a/database/migrations/2016_10_30_174400_CreateSchedulesTable.php b/database/migrations/2016_10_30_174400_CreateSchedulesTable.php new file mode 100644 index 000000000000..772119854516 --- /dev/null +++ b/database/migrations/2016_10_30_174400_CreateSchedulesTable.php @@ -0,0 +1,45 @@ +increments('id'); + $table->string('name'); + $table->longText('message')->nullable()->default(null); + $table->tinyInteger('status')->unsigned()->default(0); + $table->timestamp('scheduled_at'); + $table->timestamp('completed_at')->nullable()->default(null); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('schedules'); + } +} diff --git a/database/migrations/2016_10_30_174410_CreateScheduleComponentsTable.php b/database/migrations/2016_10_30_174410_CreateScheduleComponentsTable.php new file mode 100644 index 000000000000..0ff151d897c1 --- /dev/null +++ b/database/migrations/2016_10_30_174410_CreateScheduleComponentsTable.php @@ -0,0 +1,43 @@ +increments('id'); + $table->integer('schedule_id')->unsigned(); + $table->integer('component_id')->unsigned(); + $table->tinyInteger('component_status')->unsigned(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('schedule_components'); + } +} diff --git a/database/migrations/2016_10_30_182324_AlterTableIncidentsRemoveScheduledColumns.php b/database/migrations/2016_10_30_182324_AlterTableIncidentsRemoveScheduledColumns.php new file mode 100644 index 000000000000..625dc553dfa3 --- /dev/null +++ b/database/migrations/2016_10_30_182324_AlterTableIncidentsRemoveScheduledColumns.php @@ -0,0 +1,47 @@ +whereNotNull('scheduled_at')->delete(); + + Schema::table('incidents', function (Blueprint $table) { + $table->dropColumn('scheduled_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('incidents', function (Blueprint $table) { + $table->timestamp('scheduled_at')->after('user_id')->nullable()->default(null); + }); + } +} diff --git a/resources/lang/en/cachet.php b/resources/lang/en/cachet.php index d7eeb8e0fb27..29f38cccab48 100644 --- a/resources/lang/en/cachet.php +++ b/resources/lang/en/cachet.php @@ -36,7 +36,6 @@ 'scheduled_at' => ', scheduled :timestamp', 'posted' => 'Posted :timestamp', 'status' => [ - 0 => 'Scheduled', // TODO: Hopefully remove this. 1 => 'Investigating', 2 => 'Identified', 3 => 'Watching', @@ -44,6 +43,15 @@ ], ], + // Schedule + 'schedules' => [ + 'status' => [ + 0 => 'Upcoming', + 1 => 'In Progress', + 2 => 'Complete', + ], + ], + // Service Status 'service' => [ 'good' => '[0,1] System operational|[2,Inf] All systems are operational', diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index f85371c56290..bab106d7f0a7 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -51,7 +51,6 @@ 'component' => 'Component', 'message' => 'Message', 'message-help' => 'You may also use Markdown.', - 'scheduled_at' => 'When to schedule the maintenance for?', 'occurred_at' => 'When did this incident occur?', 'notify_subscribers' => 'Notify subscribers?', 'visibility' => 'Incident Visibility', @@ -67,6 +66,20 @@ ], ], + 'schedules' => [ + 'name' => 'Name', + 'status' => 'Status', + 'message' => 'Message', + 'message-help' => 'You may also use Markdown.', + 'scheduled_at' => 'When is this maintenance scheduled for?', + 'completed_at' => 'When did this maintenance complete?', + 'templates' => [ + 'name' => 'Name', + 'template' => 'Template', + 'twig' => 'Incident Templates can make use of the Twig templating language.', + ], + ], + // Components form fields 'components' => [ 'name' => 'Name', diff --git a/resources/views/dashboard/partials/sidebar.blade.php b/resources/views/dashboard/partials/sidebar.blade.php index b1bff1ccfbc2..2382cf292157 100644 --- a/resources/views/dashboard/partials/sidebar.blade.php +++ b/resources/views/dashboard/partials/sidebar.blade.php @@ -22,7 +22,7 @@ {{ trans('dashboard.dashboard') }} -
  • +
  • {{ trans('dashboard.incidents.incidents') }} @@ -36,6 +36,13 @@ {{ $incident_template_count }}
  • +
  • + + + {{ trans('dashboard.schedule.schedule') }} + {{ $schedule_count }} + +
  • diff --git a/resources/views/dashboard/schedule/add.blade.php b/resources/views/dashboard/schedule/add.blade.php index 62f68c7a4e1e..d880ca16daa2 100644 --- a/resources/views/dashboard/schedule/add.blade.php +++ b/resources/views/dashboard/schedule/add.blade.php @@ -16,11 +16,10 @@ @include('dashboard.partials.errors')
    -
    @if($incident_templates->count() > 0)
    - + + +
    - +
    + @foreach(trans('cachet.schedules.status') as $id => $status) + + @endforeach +
    +
    +
    - +
    - - + +
    - @if(subscribers_enabled()) -
    - +
    + +
    - @endif
    diff --git a/resources/views/dashboard/schedule/edit.blade.php b/resources/views/dashboard/schedule/edit.blade.php index 4f65329ab96e..93fa1ea0b691 100644 --- a/resources/views/dashboard/schedule/edit.blade.php +++ b/resources/views/dashboard/schedule/edit.blade.php @@ -16,11 +16,11 @@ @include('dashboard.partials.errors') - +
    @if($incident_templates->count() > 0)
    - + + +
    - +
    + @foreach(trans('cachet.schedules.status') as $id => $status) + + @endforeach +
    +
    +
    - +
    - - + + +
    +
    + +
    diff --git a/resources/views/dashboard/schedule/index.blade.php b/resources/views/dashboard/schedule/index.blade.php index 43d9df746298..2b28bf304c52 100644 --- a/resources/views/dashboard/schedule/index.blade.php +++ b/resources/views/dashboard/schedule/index.blade.php @@ -26,7 +26,7 @@
    {{ $incident->name }}
    - {{ trans('dashboard.schedule.scheduled_at', ['timestamp' => $incident->scheduled_at_iso]) }} + {{ trans('dashboard.schedule.scheduled_at', ['timestamp' => $incident->scheduled_at_formatted]) }} @if($incident->message)

    {{ Str::words($incident->message, 5) }}

    @endif diff --git a/resources/views/partials/schedule.blade.php b/resources/views/partials/schedule.blade.php index 7dca700169fb..912a58ad2e46 100644 --- a/resources/views/partials/schedule.blade.php +++ b/resources/views/partials/schedule.blade.php @@ -1,14 +1,20 @@
    -
    + diff --git a/resources/views/single-schedule.blade.php b/resources/views/single-schedule.blade.php new file mode 100644 index 000000000000..6758ff9e9895 --- /dev/null +++ b/resources/views/single-schedule.blade.php @@ -0,0 +1,35 @@ +@extends('layout.master') + +@section('bodyClass', 'no-padding') + +@section('outer-content') +@include('partials.nav') +@stop + +@section('content') +

    {{ $schedule->name }}

    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + {{ $schedule->name }}{{ trans("cachet.incidents.scheduled_at", ["timestamp" => $schedule->scheduled_at_diff]) }} +
    +
    + {!! $schedule->formattedMessage !!} +
    +
    +
    +
    +
    +
    +
    +@stop diff --git a/tests/Api/ScheduleTest.php b/tests/Api/ScheduleTest.php new file mode 100644 index 000000000000..9a97d5546af8 --- /dev/null +++ b/tests/Api/ScheduleTest.php @@ -0,0 +1,87 @@ + + */ +class ScheduleTest extends AbstractApiTestCase +{ + public function testGetSchedules() + { + $schedules = factory('CachetHQ\Cachet\Models\Schedule', 3)->create(); + + $this->get('/api/v1/schedules'); + + $this->assertResponseOk(); + + $this->seeJson(['id' => $schedules[0]->id]); + $this->seeJson(['id' => $schedules[1]->id]); + $this->seeJson(['id' => $schedules[2]->id]); + } + + public function testGetSchedule() + { + $schedule = factory('CachetHQ\Cachet\Models\Schedule')->create(); + + $this->get('/api/v1/schedules/'.$schedule->id); + + $this->assertResponseOk(); + + $this->seeJson($schedule->toArray()); + } + + public function testCreateSchedule() + { + $this->beUser(); + + $schedule = [ + 'name' => 'Test Schedule', + 'message' => 'Foo bar, baz.', + 'status' => 1, + 'scheduled_at' => date('Y-m-d H:i'), + ]; + + $this->post('/api/v1/schedules/', $schedule); + + $this->assertResponseOk(); + + $this->seeJson(array_forget($schedule, 'scheduled_at')); + } + + public function testUpdateSchedule() + { + $this->beUser(); + + $schedule = factory('CachetHQ\Cachet\Models\Schedule')->create(); + + $this->put('/api/v1/schedules/'.$schedule->id, [ + 'name' => 'Updated schedule', + ]); + + $this->assertResponseOk(); + + $this->seeJson(['name' => 'Updated schedule']); + } + + public function testDeleteSchedule() + { + $this->beUser(); + factory('CachetHQ\Cachet\Models\Schedule')->create(); + + $this->delete('/api/v1/schedules/1'); + + $this->assertResponseStatus(204); + } +} diff --git a/tests/Bus/Commands/Incident/ReportMaintenanceCommandTest.php b/tests/Bus/Commands/Incident/ReportMaintenanceCommandTest.php deleted file mode 100644 index f3ee9427c9bb..000000000000 --- a/tests/Bus/Commands/Incident/ReportMaintenanceCommandTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @author Graham Campbell - */ -class ReportMaintenanceCommandTest extends AbstractTestCase -{ - use CommandTrait; - - protected function getObjectAndParams() - { - $params = [ - 'name' => 'Test', - 'message' => 'Foo bar baz', - 'notify' => false, - 'timestamp' => '2020-12-30 00:00:01', - ]; - - $object = new ReportMaintenanceCommand( - $params['name'], - $params['message'], - $params['notify'], - $params['timestamp'] - ); - - return compact('params', 'object'); - } - - protected function objectHasRules() - { - return true; - } - - protected function getHandlerClass() - { - return ReportMaintenanceCommandHandler::class; - } -} diff --git a/tests/Bus/Commands/Schedule/CreateScheduleCommandTest.php b/tests/Bus/Commands/Schedule/CreateScheduleCommandTest.php new file mode 100644 index 000000000000..3c3896fc6948 --- /dev/null +++ b/tests/Bus/Commands/Schedule/CreateScheduleCommandTest.php @@ -0,0 +1,59 @@ + + */ +class CreateScheduleCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = [ + 'name' => 'Test', + 'message' => 'Foo', + 'status' => 1, + 'scheduled_at' => date('Y-m-d H:i'), + 'completed_at' => date('Y-m-d H:i'), + 'components' => [], + ]; + $object = new CreateScheduleCommand( + $params['name'], + $params['message'], + $params['status'], + $params['scheduled_at'], + $params['completed_at'], + $params['components'] + ); + + return compact('params', 'object'); + } + + protected function objectHasRules() + { + return true; + } + + protected function getHandlerClass() + { + return CreateScheduleCommandHandler::class; + } +} diff --git a/tests/Bus/Commands/Schedule/DeleteScheduleCommandTest.php b/tests/Bus/Commands/Schedule/DeleteScheduleCommandTest.php new file mode 100644 index 000000000000..0d5ad61f77da --- /dev/null +++ b/tests/Bus/Commands/Schedule/DeleteScheduleCommandTest.php @@ -0,0 +1,50 @@ + + */ +class DeleteScheduleCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = [ + 'schedule' => new Schedule(), + ]; + $object = new DeleteScheduleCommand( + $params['schedule'] + ); + + return compact('params', 'object'); + } + + protected function objectHasRules() + { + return true; + } + + protected function getHandlerClass() + { + return DeleteScheduleCommandHandler::class; + } +} diff --git a/tests/Bus/Commands/Schedule/UpdateScheduleCommandTest.php b/tests/Bus/Commands/Schedule/UpdateScheduleCommandTest.php new file mode 100644 index 000000000000..583f6ef76b41 --- /dev/null +++ b/tests/Bus/Commands/Schedule/UpdateScheduleCommandTest.php @@ -0,0 +1,62 @@ + + */ +class UpdateScheduleCommandTest extends AbstractTestCase +{ + use CommandTrait; + + protected function getObjectAndParams() + { + $params = [ + 'schedule' => new Schedule(), + 'name' => 'Foo', + 'message' => 'Bar', + 'status' => 1, + 'scheduled_at' => date('Y-m-d H:i'), + 'completed_at' => date('Y-m-d H:i'), + 'components' => [], + ]; + $object = new UpdateScheduleCommand( + $params['schedule'], + $params['name'], + $params['message'], + $params['status'], + $params['scheduled_at'], + $params['completed_at'], + $params['components'] + ); + + return compact('params', 'object'); + } + + protected function objectHasRules() + { + return true; + } + + protected function getHandlerClass() + { + return UpdateScheduleCommandHandler::class; + } +} diff --git a/tests/Bus/Events/Incident/MaintenanceWasScheduledEventTest.php b/tests/Bus/Events/Incident/MaintenanceWasScheduledEventTest.php deleted file mode 100644 index bf273cf06510..000000000000 --- a/tests/Bus/Events/Incident/MaintenanceWasScheduledEventTest.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ -class MaintenanceWasScheduledEventTest extends AbstractIncidentEventTestCase -{ - protected function objectHasHandlers() - { - return true; - } - - protected function getObjectAndParams() - { - $params = ['incident' => new Incident()]; - $object = new MaintenanceWasScheduledEvent($params['incident']); - - return compact('params', 'object'); - } -} diff --git a/tests/Bus/Events/Schedule/AbstractScheduleEventTestCase.php b/tests/Bus/Events/Schedule/AbstractScheduleEventTestCase.php new file mode 100644 index 000000000000..66b805a16b6b --- /dev/null +++ b/tests/Bus/Events/Schedule/AbstractScheduleEventTestCase.php @@ -0,0 +1,26 @@ + new Schedule()]; + $object = new ScheduleWasCreatedEvent($params['schedule']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/Schedule/ScheduleWasRemovedEventTest.php b/tests/Bus/Events/Schedule/ScheduleWasRemovedEventTest.php new file mode 100644 index 000000000000..2989e27f0e3a --- /dev/null +++ b/tests/Bus/Events/Schedule/ScheduleWasRemovedEventTest.php @@ -0,0 +1,31 @@ + new Schedule()]; + $object = new ScheduleWasRemovedEvent($params['schedule']); + + return compact('params', 'object'); + } +} diff --git a/tests/Bus/Events/Schedule/ScheduleWasUpdatedEventTest.php b/tests/Bus/Events/Schedule/ScheduleWasUpdatedEventTest.php new file mode 100644 index 000000000000..75604788c173 --- /dev/null +++ b/tests/Bus/Events/Schedule/ScheduleWasUpdatedEventTest.php @@ -0,0 +1,31 @@ + new Schedule()]; + $object = new ScheduleWasUpdatedEvent($params['schedule']); + + return compact('params', 'object'); + } +} diff --git a/tests/Models/ScheduleComponentTest.php b/tests/Models/ScheduleComponentTest.php new file mode 100644 index 000000000000..020412af636c --- /dev/null +++ b/tests/Models/ScheduleComponentTest.php @@ -0,0 +1,31 @@ + + */ +class ScheduleComponentTest extends AbstractTestCase +{ + use ValidationTrait; + + public function testValidation() + { + $this->checkRules(new ScheduleComponent()); + } +} diff --git a/tests/Models/ScheduleTest.php b/tests/Models/ScheduleTest.php new file mode 100644 index 000000000000..cf679557ae87 --- /dev/null +++ b/tests/Models/ScheduleTest.php @@ -0,0 +1,31 @@ + + */ +class ScheduleTest extends AbstractTestCase +{ + use ValidationTrait; + + public function testValidation() + { + $this->checkRules(new Schedule()); + } +}