Schedule import: Add overview

This commit is contained in:
Igor Scheller 2020-11-21 20:54:04 +01:00 committed by msquare
parent 251f2cbfa6
commit ebab34ee67
17 changed files with 346 additions and 106 deletions

View File

@ -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');
}
);

View File

@ -0,0 +1,76 @@
<?php
namespace Engelsystem\Migrations;
use Carbon\Carbon;
use Engelsystem\Database\Migration\Migration;
use Engelsystem\Models\Shifts\Schedule;
use Illuminate\Database\Schema\Blueprint;
class AddNameMinutesAndTimestampsToSchedules extends Migration
{
use Reference;
/**
* Run the migration
*/
public function up()
{
$this->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();
}
);
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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."

View File

@ -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)"

View File

@ -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."

View File

@ -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)"

View File

@ -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 %}
<div class="col-md-12">
<p>{{ __('schedule.last_update', [schedule.updated_at.format(__('Y-m-d H:i'))]) }}</p>
</div>
{% endif %}
<form method="post">
{{ csrf() }}
<div class="col-lg-12">
{{ 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')) }}
</div>
</form>
{% endblock %}

View File

@ -6,35 +6,56 @@
{% block content %}
<div class="container">
<h1>{% block content_title %}{{ title }}{% endblock %}</h1>
<h1>
{% 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 %}
</h1>
{% include 'layouts/parts/messages.twig' %}
<div class="row">
{% block row_content %}
<form method="POST" action="{{ url('/admin/schedule/load') }}">
{{ csrf() }}
<div class="col-md-12">
<p>{{ __('schedule.import.text') }}</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{{ __('schedule.name') }}</th>
<th>{{ __('schedule.url') }}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for schedule in schedules %}
<tr>
<td>{{ schedule.name }}</td>
<td>{{ schedule.url }}</td>
<td>
<div class="btn-group">
<a
href="{{ url('/admin/schedule/load/' ~ schedule.id) }}"
class="btn btn-xs btn-default">
{{ __('form.import') }}
</a>
<a
href="{{ url('/admin/schedule/edit/' ~ schedule.id) }}"
class="btn btn-xs btn-default">
{{ __('form.edit') }}
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-lg-6">
{{ 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')) }}
</div>
</form>
{% endblock %}
</div>
</div>

View File

@ -4,12 +4,8 @@
{% block title %}{{ __('schedule.import.load.title') }}{% endblock %}
{% block row_content %}
<form method="POST" action="{{ url('/admin/schedule/import') }}">
<form method="POST" action="{{ url('/admin/schedule/import/' ~ schedule_id) }}">
{{ 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) }}
<div class="col-lg-12">
<p>{{ __('schedule.import.load.info', [schedule.conference.title, schedule.version]) }}</p>

View File

@ -43,7 +43,7 @@
{% endif %}
<select id="{{ name }}" name="{{ name }}" class="form-control">
{% for value,decription in data -%}
<option value="{{ value }}" {% if name == selected %} selected{% endif %}>{{ decription }}</option>
<option value="{{ value }}" {% if value == selected %} selected{% endif %}>{{ decription }}</option>
{% endfor %}
</select>
</div>

View File

@ -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 */

View File

@ -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

View File

@ -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);
}
/**

View File

@ -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']);

View File

@ -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();