diff --git a/config/routes.php b/config/routes.php index 3c4ed5d4..e51df17a 100644 --- a/config/routes.php +++ b/config/routes.php @@ -57,8 +57,10 @@ $route->addGroup( '/schedule', function (RouteCollector $route) { $route->get('', 'Admin\\Schedule\\ImportSchedule@index'); - $route->post('/load', 'Admin\\Schedule\\ImportSchedule@loadSchedule'); - $route->post('/import', 'Admin\\Schedule\\ImportSchedule@importSchedule'); + $route->get('/edit[/{id:\d+}]', 'Admin\\Schedule\\ImportSchedule@edit'); + $route->post('/edit[/{id:\d+}]', 'Admin\\Schedule\\ImportSchedule@save'); + $route->get('/load/{id:\d+}', 'Admin\\Schedule\\ImportSchedule@loadSchedule'); + $route->post('/import/{id:\d+}', 'Admin\\Schedule\\ImportSchedule@importSchedule'); } ); diff --git a/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php b/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php new file mode 100644 index 00000000..fcd41c1c --- /dev/null +++ b/db/migrations/2020_11_20_000000_add_name_minutes_and_timestamps_to_schedules.php @@ -0,0 +1,76 @@ +schema->table( + 'schedules', + function (Blueprint $table) { + $table->string('name')->after('id'); + $table->integer('shift_type')->after('name'); + $table->integer('minutes_before')->after('shift_type'); + $table->integer('minutes_after')->after('minutes_before'); + $table->timestamps(); + } + ); + + Schedule::query() + ->update([ + 'created_at' => Carbon::now(), + 'minutes_before' => 15, + 'minutes_after' => 15, + ]); + + // Add legacy reference + if ($this->schema->hasTable('ShiftTypes')) { + $connection = $this->schema->getConnection(); + $query = $connection + ->table('Shifts') + ->select('Shifts.shifttype_id') + ->join('schedule_shift', 'Shifts.SID', 'schedule_shift.shift_id') + ->where('schedule_shift.schedule_id', $connection->raw('schedules.id')) + ->limit(1); + + Schedule::query() + ->update(['shift_type' => $connection->raw('(' . $query->toSql() . ')')]); + + $this->schema->table( + 'schedules', + function (Blueprint $table) { + $this->addReference($table, 'shift_type', 'ShiftTypes'); + } + ); + } + } + + /** + * Reverse the migration + */ + public function down() + { + $this->schema->table( + 'schedules', + function (Blueprint $table) { + $table->dropForeign('schedules_shift_type_foreign'); + $table->dropColumn('name'); + $table->dropColumn('shift_type'); + $table->dropColumn('minutes_before'); + $table->dropColumn('minutes_after'); + $table->dropTimestamps(); + } + ); + } +} diff --git a/db/migrations/Reference.php b/db/migrations/Reference.php index d0550686..7d08ab02 100644 --- a/db/migrations/Reference.php +++ b/db/migrations/Reference.php @@ -22,6 +22,7 @@ trait Reference * @param string $targetTable * @param string|null $fromColumn * @param bool $setPrimary + * * @return ColumnDefinition */ protected function references( @@ -37,11 +38,21 @@ trait Reference $table->primary($fromColumn); } + $this->addReference($table, $fromColumn, $targetTable); + + return $col; + } + + /** + * @param Blueprint $table + * @param string $fromColumn + * @param string $targetTable + */ + protected function addReference(Blueprint $table, string $fromColumn, string $targetTable) + { $table->foreign($fromColumn) ->references('id')->on($targetTable) ->onUpdate('cascade') ->onDelete('cascade'); - - return $col; } } diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php index 0e50dba6..455f445c 100644 --- a/includes/pages/schedule/ImportSchedule.php +++ b/includes/pages/schedule/ImportSchedule.php @@ -6,6 +6,8 @@ namespace Engelsystem\Controllers\Admin\Schedule; use Carbon\Carbon; use Engelsystem\Controllers\BaseController; +use Engelsystem\Controllers\CleanupModel; +use Engelsystem\Controllers\HasUserNotifications; use Engelsystem\Helpers\Schedule\Event; use Engelsystem\Helpers\Schedule\Room; use Engelsystem\Helpers\Schedule\Schedule; @@ -20,7 +22,6 @@ use GuzzleHttp\Client as GuzzleClient; use Illuminate\Database\Connection as DatabaseConnection; use Illuminate\Database\Eloquent\Builder as QueryBuilder; use Illuminate\Database\Eloquent\Collection as DatabaseCollection; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Psr\Log\LoggerInterface; use stdClass; @@ -28,6 +29,9 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; class ImportSchedule extends BaseController { + use CleanupModel; + use HasUserNotifications; + /** @var DatabaseConnection */ protected $db; @@ -86,11 +90,76 @@ class ImportSchedule extends BaseController return $this->response->withView( 'admin/schedule/index.twig', [ - 'errors' => $this->getFromSession('errors'), - 'success' => $this->getFromSession('success'), + 'is_index' => true, + 'schedules' => ScheduleUrl::all(), + ] + $this->getNotifications() + ); + } + + /** + * @param Request $request + * + * @return Response + */ + public function edit(Request $request): Response + { + $schedule = ScheduleUrl::find($request->getAttribute('id')); + $this->cleanupModelNullValues($schedule); + + return $this->response->withView( + 'admin/schedule/edit.twig', + [ + 'schedule' => $schedule, 'shift_types' => $this->getShiftTypes(), + ] + $this->getNotifications() + ); + } + + /** + * @param Request $request + * + * @return Response + */ + public function save(Request $request): Response + { + $id = $request->getAttribute('id'); + /** @var ScheduleUrl $schedule */ + $schedule = ScheduleUrl::findOrNew($id); + + $data = $this->validate($request, [ + 'name' => 'required', + 'url' => 'required', + 'shift_type' => 'required|int', + 'minutes_before' => 'required|int', + 'minutes_after' => 'required|int', + ]); + + if (!isset($this->getShiftTypes()[$data['shift_type']])) { + throw new ErrorException('schedule.import.invalid-shift-type'); + } + + $schedule->name = $data['name']; + $schedule->url = $data['url']; + $schedule->shift_type = $data['shift_type']; + $schedule->minutes_before = $data['minutes_before']; + $schedule->minutes_after = $data['minutes_after']; + + $schedule->save(); + + $this->log->info( + 'Schedule {name}: Url {url}, Shift Type {shift_type}, minutes before/after {before}/{after}', + [ + 'name' => $schedule->name, + 'url' => $schedule->name, + 'shift_type' => $schedule->shift_type, + 'before' => $schedule->minutes_before, + 'after' => $schedule->minutes_after, ] ); + + $this->addNotification('schedule.edit.success'); + + return redirect('/admin/schedule/load/' . $schedule->id); } /** @@ -116,24 +185,19 @@ class ImportSchedule extends BaseController $changeEvents, $deleteEvents, $newRooms, - $shiftType, + , $scheduleUrl, - $schedule, - $minutesBefore, - $minutesAfter + $schedule ) = $this->getScheduleData($request); } catch (ErrorException $e) { - return back()->with('errors', [$e->getMessage()]); + $this->addNotification($e->getMessage(), 'errors'); + return back(); } return $this->response->withView( 'admin/schedule/load.twig', [ - 'errors' => $this->getFromSession('errors'), - 'schedule_url' => $scheduleUrl->url, - 'shift_type' => $shiftType, - 'minutes_before' => $minutesBefore, - 'minutes_after' => $minutesAfter, + 'schedule_id' => $scheduleUrl->id, 'schedule' => $schedule, 'rooms' => [ 'add' => $newRooms, @@ -143,7 +207,7 @@ class ImportSchedule extends BaseController 'update' => $changeEvents, 'delete' => $deleteEvents, ], - ] + ] + $this->getNotifications() ); } @@ -172,10 +236,11 @@ class ImportSchedule extends BaseController $scheduleUrl ) = $this->getScheduleData($request); } catch (ErrorException $e) { - return back()->with('errors', [$e->getMessage()]); + $this->addNotification($e->getMessage(), 'errors'); + return back(); } - $this->log('Started schedule "{schedule}" import', ['schedule' => $scheduleUrl->url]); + $this->log('Started schedule "{name}" import', ['name' => $scheduleUrl->name]); foreach ($newRooms as $room) { $this->createRoom($room); @@ -207,10 +272,11 @@ class ImportSchedule extends BaseController $this->deleteEvent($event); } - $this->log('Ended schedule "{schedule}" import', ['schedule' => $scheduleUrl->url]); + $scheduleUrl->touch(); + $this->log('Ended schedule "{name}" import', ['name' => $scheduleUrl->name]); return redirect($this->url, 303) - ->with('success', ['schedule.import.success']); + ->with('messages', ['schedule.import.success']); } /** @@ -337,17 +403,11 @@ class ImportSchedule extends BaseController */ protected function getScheduleData(Request $request) { - $data = $this->validate( - $request, - [ - 'schedule-url' => 'required|url', - 'shift-type' => 'required|int', - 'minutes-before' => 'optional|int', - 'minutes-after' => 'optional|int', - ] - ); + $id = $request->getAttribute('id'); + /** @var ScheduleUrl $scheduleUrl */ + $scheduleUrl = ScheduleUrl::findOrFail($id); - $scheduleResponse = $this->guzzle->get($data['schedule-url']); + $scheduleResponse = $this->guzzle->get($scheduleUrl->url); if ($scheduleResponse->getStatusCode() != 200) { throw new ErrorException('schedule.import.request-error'); } @@ -357,15 +417,10 @@ class ImportSchedule extends BaseController throw new ErrorException('schedule.import.read-error'); } - $shiftType = (int)$data['shift-type']; - if (!isset($this->getShiftTypes()[$shiftType])) { - throw new ErrorException('schedule.import.invalid-shift-type'); - } - - $scheduleUrl = $this->getScheduleUrl($data['schedule-url']); + $shiftType = $scheduleUrl->shift_type; $schedule = $this->parser->getSchedule(); - $minutesBefore = isset($data['minutes-before']) ? (int)$data['minutes-before'] : 15; - $minutesAfter = isset($data['minutes-after']) ? (int)$data['minutes-after'] : 15; + $minutesBefore = $scheduleUrl->minutes_before; + $minutesAfter = $scheduleUrl->minutes_after; $newRooms = $this->newRooms($schedule->getRooms()); return array_merge( $this->shiftsDiff($schedule, $scheduleUrl, $shiftType, $minutesBefore, $minutesAfter), @@ -373,18 +428,6 @@ class ImportSchedule extends BaseController ); } - /** - * @param string $name - * @return Collection - */ - protected function getFromSession(string $name): Collection - { - $data = Collection::make(Arr::flatten($this->session->get($name, []))); - $this->session->remove($name); - - return $data; - } - /** * @param Room[] $scheduleRooms * @return Room[] @@ -581,22 +624,6 @@ class ImportSchedule extends BaseController return $return; } - /** - * @param string $scheduleUrl - * @return ScheduleUrl - */ - protected function getScheduleUrl(string $scheduleUrl): ScheduleUrl - { - if (!$schedule = ScheduleUrl::whereUrl($scheduleUrl)->first()) { - $schedule = new ScheduleUrl(['url' => $scheduleUrl]); - $schedule->save(); - - $this->log('Created schedule "{schedule}"', ['schedule' => $schedule->url]); - } - - return $schedule; - } - /** * @param string $message * @param array $context diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index 42f6ad95..c3d383d5 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -35,8 +35,8 @@ msgstr "Deine Passwörter stimmen nicht überein." msgid "validation.password_confirmation.required" msgstr "Du musst dein Passwort bestätigen." -msgid "schedule.import" -msgstr "Programm importieren" +msgid "schedule.edit.success" +msgstr "Das Programm wurde erfolgreich konfiguriert." msgid "schedule.import.request-error" msgstr "Das Programm konnte nicht abgerufen werden." diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 8edc5dfe..17623360 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2787,12 +2787,27 @@ msgstr "Programm laden" msgid "form.import" msgstr "Importieren" +msgid "form.edit" +msgstr "Bearbeiten" + +msgid "form.save" +msgstr "Speichern" + +msgid "schedule.import" +msgstr "Programm importieren" + +msgid "schedule.edit.title" +msgstr "Programm bearbeiten" + msgid "schedule.import.title" msgstr "Programm importieren" +msgid "schedule.last_update" +msgstr "Aktualisiert am: %s" + msgid "schedule.import.text" msgstr "" -"Dieser Import erstellt Räume und erstellt, aktualisiert und löscht Schichten anhand des schedule.xml exportes." +"Importe erstellen Räume und erstellen, aktualisieren und löschen Schichten anhand eines schedule.xml exportes." msgid "schedule.import.load.title" msgstr "Programm importieren: Vorschau" @@ -2800,6 +2815,9 @@ msgstr "Programm importieren: Vorschau" msgid "schedule.import.load.info" msgstr "Importiere \"%s\" (Version \"%s\")" +msgid "schedule.name" +msgstr "Programm Name" + msgid "schedule.url" msgstr "Programm URL (schedule.xml)" diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index e5baf48c..fbdb0f34 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -33,8 +33,8 @@ msgstr "Your passwords are not equal." msgid "validation.password_confirmation.required" msgstr "You have to confirm your password." -msgid "schedule.import" -msgstr "Import schedule" +msgid "schedule.edit.success" +msgstr "The schedule was configured successfully." msgid "schedule.import.request-error" msgstr "The schedule could not be requested." diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 5ed475e3..e6d39720 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -52,11 +52,26 @@ msgstr "Load schedule" msgid "form.import" msgstr "Import" +msgid "form.save" +msgstr "Save" + +msgid "form.edit" +msgstr "Bearbeiten" + +msgid "schedule.import" +msgstr "Import schedule" + +msgid "schedule.edit.title" +msgstr "Edit schedule" + msgid "schedule.import.title" msgstr "Import schedule" +msgid "schedule.last_update" +msgstr "Last updated: %s" + msgid "schedule.import.text" -msgstr "This import creates rooms and creates, updates and deletes shifts according to the schedule.xml export." +msgstr "Imports create rooms and create, update and delete shifts according to a schedule.xml export." msgid "schedule.import.load.title" msgstr "Import schedule: Preview" @@ -64,6 +79,9 @@ msgstr "Import schedule: Preview" msgid "schedule.import.load.info" msgstr "Import \"%s\" (version \"%s\")" +msgid "schedule.name" +msgstr "Programm name" + msgid "schedule.url" msgstr "Schedule URL (schedule.xml)" diff --git a/resources/views/admin/schedule/edit.twig b/resources/views/admin/schedule/edit.twig new file mode 100644 index 00000000..335b34e9 --- /dev/null +++ b/resources/views/admin/schedule/edit.twig @@ -0,0 +1,29 @@ +{% extends 'admin/schedule/index.twig' %} +{% import 'macros/base.twig' as m %} +{% import 'macros/form.twig' as f %} + +{% block title %}{{ schedule ? __('schedule.edit.title') : __('schedule.import.title') }}{% endblock %} + +{% block row_content %} + {% if schedule and schedule.updated_at %} +
+

{{ __('schedule.last_update', [schedule.updated_at.format(__('Y-m-d H:i'))]) }}

+
+ {% endif %} + +
+ {{ csrf() }} + +
+ {{ f.input('name', __('schedule.name'), null, {'required': true, 'value': schedule ? schedule.name : ''}) }} + {{ f.input('url', __('schedule.url'), 'url', {'required': true, 'value': schedule ? schedule.url : ''}) }} + + {{ f.select('shift_type', shift_types|default([]), __('schedule.shift-type'), schedule ? schedule.shift_type : '') }} + + {{ f.input('minutes_before', __('schedule.minutes-before'), 'number', {'required': true, 'value': schedule ? schedule.minutes_before : 15}) }} + {{ f.input('minutes_after', __('schedule.minutes-after'), 'number', {'required': true, 'value': schedule ? schedule.minutes_after : 15}) }} + + {{ f.submit(__('form.save')) }} +
+
+{% endblock %} diff --git a/resources/views/admin/schedule/index.twig b/resources/views/admin/schedule/index.twig index 08a9cb2b..99a1a226 100644 --- a/resources/views/admin/schedule/index.twig +++ b/resources/views/admin/schedule/index.twig @@ -6,35 +6,56 @@ {% block content %}
-

{% block content_title %}{{ title }}{% endblock %}

+

+ {% block content_title %}{{ title }}{% endblock %} - {% for message in errors|default([]) %} - {{ m.alert(__(message), 'danger') }} - {% endfor %} - {% for message in success|default([]) %} - {{ m.alert(__(message), 'success') }} - {% endfor %} + {% if is_index|default(false) %} + {{ m.button(m.glyphicon('plus'), url('/admin/schedule/edit')) }} + {% endif %} +

+ + {% include 'layouts/parts/messages.twig' %}
{% block row_content %} -
- {{ csrf() }} +
+

{{ __('schedule.import.text') }}

-
-

{{ __('schedule.import.text') }}

+
+ + + + + + + + + + + {% for schedule in schedules %} + + + + + + {% endfor %} + +
{{ __('schedule.name') }}{{ __('schedule.url') }}
{{ schedule.name }}{{ schedule.url }} + +
- -
- {{ f.input('schedule-url', __('schedule.url'), 'url', {'required': true}) }} - - {{ f.select('shift-type', shift_types|default([]), __('schedule.shift-type')) }} - - {{ f.input('minutes-before', __('schedule.minutes-before'), 'number', {'value': 15, 'required': true}) }} - {{ f.input('minutes-after', __('schedule.minutes-after'), 'number', {'value': 15, 'required': true}) }} - - {{ f.submit(__('form.load_schedule')) }} -
- +
{% endblock %}
diff --git a/resources/views/admin/schedule/load.twig b/resources/views/admin/schedule/load.twig index 8c936bec..2d09d771 100644 --- a/resources/views/admin/schedule/load.twig +++ b/resources/views/admin/schedule/load.twig @@ -4,12 +4,8 @@ {% block title %}{{ __('schedule.import.load.title') }}{% endblock %} {% block row_content %} -
+ {{ csrf() }} - {{ f.hidden('schedule-url', schedule_url) }} - {{ f.hidden('shift-type', shift_type) }} - {{ f.hidden('minutes-before', minutes_before) }} - {{ f.hidden('minutes-after', minutes_after) }}

{{ __('schedule.import.load.info', [schedule.conference.title, schedule.version]) }}

diff --git a/resources/views/macros/form.twig b/resources/views/macros/form.twig index 9e9a7483..07db74ad 100644 --- a/resources/views/macros/form.twig +++ b/resources/views/macros/form.twig @@ -43,7 +43,7 @@ {% endif %}
diff --git a/src/Controllers/CleanupModel.php b/src/Controllers/CleanupModel.php index b1f28f8b..d7173b3a 100644 --- a/src/Controllers/CleanupModel.php +++ b/src/Controllers/CleanupModel.php @@ -17,6 +17,10 @@ trait CleanupModel */ protected function cleanupModelNullValues($models, array $attributes = []) { + if (!$models) { + return; + } + $models = $models instanceof Model ? [$models] : $models; foreach ($models as $model) { /** @var Model $model */ diff --git a/src/Models/Shifts/Schedule.php b/src/Models/Shifts/Schedule.php index c1eb2d9e..3f58bcf6 100644 --- a/src/Models/Shifts/Schedule.php +++ b/src/Models/Shifts/Schedule.php @@ -2,6 +2,7 @@ namespace Engelsystem\Models\Shifts; +use Carbon\Carbon; use Engelsystem\Models\BaseModel; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -9,17 +10,38 @@ use Illuminate\Database\Query\Builder as QueryBuilder; /** * @property int $id + * @property string $name * @property string $url + * @property int $shift_type + * @property int $minutes_before + * @property int $minutes_after + * @property Carbon $created_at + * @property Carbon $updated_at * * @property-read QueryBuilder|Collection|ScheduleShift[] $scheduleShifts * * @method static QueryBuilder|Schedule[] whereId($value) + * @method static QueryBuilder|Schedule[] whereName($value) * @method static QueryBuilder|Schedule[] whereUrl($value) + * @method static QueryBuilder|Schedule[] whereShiftType($value) + * @method static QueryBuilder|Schedule[] whereMinutesBefore($value) + * @method static QueryBuilder|Schedule[] whereMinutesAfter($value) + * @method static QueryBuilder|Schedule[] whereCreatedAt($value) + * @method static QueryBuilder|Schedule[] whereUpdatedAt($value) */ class Schedule extends BaseModel { + /** @var bool enable timestamps */ + public $timestamps = true; + /** @var array Values that are mass assignable */ - protected $fillable = ['url']; + protected $fillable = [ + 'name', + 'url', + 'shift_type', + 'minutes_before', + 'minutes_after', + ]; /** * @return HasMany diff --git a/tests/Unit/Controllers/CleanupModelTest.php b/tests/Unit/Controllers/CleanupModelTest.php index 3a25f136..863df18e 100644 --- a/tests/Unit/Controllers/CleanupModelTest.php +++ b/tests/Unit/Controllers/CleanupModelTest.php @@ -22,10 +22,12 @@ class CleanupModelTest extends TestCase $model->foo = null; $model2 = new TestModel(); $model3 = new TestModel(); + $model4 = null; $cleanup->cleanup($model); $cleanup->cleanup([$model2]); $cleanup->cleanup($model3, ['text']); + $cleanup->cleanup($model4); $this->assertTrue(isset($model->text)); $this->assertTrue(isset($model->created_at)); @@ -37,6 +39,8 @@ class CleanupModelTest extends TestCase $this->assertTrue(isset($model3->text)); $this->assertNull($model3->another_text); $this->assertNull($model3->foo); + + $this->assertNull($model4); } /** diff --git a/tests/Unit/Models/Shifts/ScheduleShiftTest.php b/tests/Unit/Models/Shifts/ScheduleShiftTest.php index e8626993..4c861ad9 100644 --- a/tests/Unit/Models/Shifts/ScheduleShiftTest.php +++ b/tests/Unit/Models/Shifts/ScheduleShiftTest.php @@ -14,7 +14,13 @@ class ScheduleShiftTest extends ModelTest */ public function testScheduleShifts() { - $schedule = new Schedule(['url' => 'https://lorem.ipsum/schedule.xml']); + $schedule = new Schedule([ + 'url' => 'https://lorem.ipsum/schedule.xml', + 'name' => 'Test', + 'shift_type' => 0, + 'minutes_before' => 15, + 'minutes_after' => 15, + ]); $schedule->save(); $scheduleShift = new ScheduleShift(['shift_id' => 1, 'guid' => 'a']); diff --git a/tests/Unit/Models/Shifts/ScheduleTest.php b/tests/Unit/Models/Shifts/ScheduleTest.php index 22ba6083..496b16b1 100644 --- a/tests/Unit/Models/Shifts/ScheduleTest.php +++ b/tests/Unit/Models/Shifts/ScheduleTest.php @@ -13,7 +13,13 @@ class ScheduleTest extends ModelTest */ public function testScheduleShifts() { - $schedule = new Schedule(['url' => 'https://foo.bar/schedule.xml']); + $schedule = new Schedule([ + 'url' => 'https://foo.bar/schedule.xml', + 'name' => 'Testing', + 'shift_type' => 0, + 'minutes_before' => 10, + 'minutes_after' => 10, + ]); $schedule->save(); (new ScheduleShift(['shift_id' => 1, 'schedule_id' => $schedule->id, 'guid' => 'a']))->save();