From 42721e95726559b4a601240bb5b0fe4e5d755b2a Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Wed, 27 Nov 2019 23:43:21 +0100 Subject: Added Schedule parsing and replaced old Fahrplan importer Resolves #553 (Change Frab Import from xCal to XML) Resolves #538 (Feature Request: Multi Frab Import) --- includes/pages/schedule/ImportSchedule.php | 612 +++++++++++++++++++++++++++++ 1 file changed, 612 insertions(+) create mode 100644 includes/pages/schedule/ImportSchedule.php (limited to 'includes/pages/schedule/ImportSchedule.php') diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php new file mode 100644 index 00000000..1b03b57b --- /dev/null +++ b/includes/pages/schedule/ImportSchedule.php @@ -0,0 +1,612 @@ +guzzle = $guzzle; + $this->parser = $parser; + $this->response = $response; + $this->session = $session; + $this->db = $db; + $this->log = $log; + } + + /** + * @return Response + */ + public function index(): Response + { + return $this->response->withView( + 'admin/schedule/index.twig', + [ + 'errors' => $this->getFromSession('errors'), + 'success' => $this->getFromSession('success'), + 'shift_types' => $this->getShiftTypes(), + ] + ); + } + + /** + * @param Request $request + * @return Response + */ + public function loadSchedule(Request $request): Response + { + try { + /** + * @var Event[] $newEvents + * @var Event[] $changeEvents + * @var Event[] $deleteEvents + * @var Room[] $newRooms + * @var int $shiftType + * @var ScheduleUrl $scheduleUrl + * @var Schedule $schedule + * @var int $minutesBefore + * @var int $minutesAfter + */ + list( + $newEvents, + $changeEvents, + $deleteEvents, + $newRooms, + $shiftType, + $scheduleUrl, + $schedule, + $minutesBefore, + $minutesAfter + ) = $this->getScheduleData($request); + } catch (ErrorException $e) { + return back()->with('errors', [$e->getMessage()]); + } + + 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' => $schedule, + 'rooms' => [ + 'add' => $newRooms, + ], + 'shifts' => [ + 'add' => $newEvents, + 'update' => $changeEvents, + 'delete' => $deleteEvents, + ], + ] + ); + } + + /** + * @param Request $request + * + * @return Response + */ + public function importSchedule(Request $request): Response + { + try { + /** + * @var Event[] $newEvents + * @var Event[] $changeEvents + * @var Event[] $deleteEvents + * @var Room[] $newRooms + * @var int $shiftType + * @var ScheduleUrl $scheduleUrl + */ + list( + $newEvents, + $changeEvents, + $deleteEvents, + $newRooms, + $shiftType, + $scheduleUrl + ) = $this->getScheduleData($request); + } catch (ErrorException $e) { + return back()->with('errors', [$e->getMessage()]); + } + + $this->log('Started schedule "{schedule}" import', ['schedule' => $scheduleUrl->url]); + + foreach ($newRooms as $room) { + $this->createRoom($room); + } + + $rooms = $this->getAllRooms(); + foreach ($newEvents as $event) { + $this->createEvent( + $event, + (int)$shiftType, + $rooms + ->where('name', $event->getRoom()->getName()) + ->first(), + $scheduleUrl + ); + } + + foreach ($changeEvents as $event) { + $this->updateEvent( + $event, + (int)$shiftType, + $rooms + ->where('name', $event->getRoom()->getName()) + ->first() + ); + } + + foreach ($deleteEvents as $event) { + $this->deleteEvent($event); + } + + $this->log('Ended schedule "{schedule}" import', ['schedule' => $scheduleUrl->url]); + + return redirect($this->url, 303) + ->with('success', ['schedule.import.success']); + } + + /** + * @param Room $room + */ + protected function createRoom(Room $room): void + { + $this->db + ->table('Room') + ->insert( + [ + 'Name' => $room->getName(), + ] + ); + + $this->log('Created schedule room "{room}"', ['room' => $room->getName()]); + } + + /** + * @param Event $shift + * @param int $shiftTypeId + * @param stdClass $room + * @param ScheduleUrl $scheduleUrl + */ + protected function createEvent(Event $shift, int $shiftTypeId, stdClass $room, ScheduleUrl $scheduleUrl): void + { + $user = auth()->user(); + + $this->db + ->table('Shifts') + ->insert( + [ + 'title' => $shift->getTitle(), + 'shifttype_id' => $shiftTypeId, + 'start' => $shift->getDate()->unix(), + 'end' => $shift->getEndDate()->unix(), + 'RID' => $room->id, + 'URL' => $shift->getUrl(), + 'created_by_user_id' => $user->id, + 'created_at_timestamp' => time(), + 'edited_by_user_id' => null, + 'edited_at_timestamp' => 0, + ] + ); + + $shiftId = $this->db->getDoctrineConnection()->lastInsertId(); + + $scheduleShift = new ScheduleShift(['shift_id' => $shiftId, 'guid' => $shift->getGuid()]); + $scheduleShift->schedule()->associate($scheduleUrl); + $scheduleShift->save(); + + $this->log( + 'Created schedule shift "{shift}" in "{room}" ({from} {to}, {guid})', + [ + 'shift' => $shift->getTitle(), + 'room' => $room->name, + 'from' => $shift->getDate()->format(Carbon::RFC3339), + 'to' => $shift->getEndDate()->format(Carbon::RFC3339), + 'guid' => $shift->getGuid(), + ] + ); + } + + /** + * @param Event $shift + * @param int $shiftTypeId + * @param stdClass $room + */ + protected function updateEvent(Event $shift, int $shiftTypeId, stdClass $room): void + { + $user = auth()->user(); + + $this->db + ->table('Shifts') + ->join('schedule_shift', 'Shifts.SID', 'schedule_shift.shift_id') + ->where('schedule_shift.guid', $shift->getGuid()) + ->update( + [ + 'title' => $shift->getTitle(), + 'shifttype_id' => $shiftTypeId, + 'start' => $shift->getDate()->unix(), + 'end' => $shift->getEndDate()->unix(), + 'RID' => $room->id, + 'URL' => $shift->getUrl(), + 'edited_by_user_id' => $user->id, + 'edited_at_timestamp' => time(), + ] + ); + + $this->log( + 'Updated schedule shift "{shift}" in "{room}" ({from} {to}, {guid})', + [ + 'shift' => $shift->getTitle(), + 'room' => $room->name, + 'from' => $shift->getDate()->format(Carbon::RFC3339), + 'to' => $shift->getEndDate()->format(Carbon::RFC3339), + 'guid' => $shift->getGuid(), + ] + ); + } + + /** + * @param Event $shift + */ + protected function deleteEvent(Event $shift): void + { + $this->db + ->table('Shifts') + ->join('schedule_shift', 'Shifts.SID', 'schedule_shift.shift_id') + ->where('schedule_shift.guid', $shift->getGuid()) + ->delete(); + + $this->log( + 'Deleted schedule shift "{shift}" ({from} {to}, {guid})', + [ + 'shift' => $shift->getTitle(), + 'from' => $shift->getDate()->format(Carbon::RFC3339), + 'to' => $shift->getEndDate()->format(Carbon::RFC3339), + 'guid' => $shift->getGuid(), + ] + ); + } + + /** + * @param Request $request + * @return Event[]|Room[]|ScheduleUrl|Schedule|string + * @throws ErrorException + */ + 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', + ] + ); + + $scheduleResponse = $this->guzzle->get($data['schedule-url']); + if ($scheduleResponse->getStatusCode() != 200) { + throw new ErrorException('schedule.import.request-error'); + } + + $scheduleData = (string)$scheduleResponse->getBody(); + if (!$this->parser->load($scheduleData)) { + 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']); + $schedule = $this->parser->getSchedule(); + $minutesBefore = isset($data['minutes-before']) ? (int)$data['minutes-before'] : 15; + $minutesAfter = isset($data['minutes-after']) ? (int)$data['minutes-after'] : 15; + $newRooms = $this->newRooms($schedule->getRooms()); + return array_merge( + $this->shiftsDiff($schedule, $scheduleUrl, $shiftType, $minutesBefore, $minutesAfter), + [$newRooms, $shiftType, $scheduleUrl, $schedule, $minutesBefore, $minutesAfter] + ); + } + + /** + * @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[] + */ + protected function newRooms(array $scheduleRooms): array + { + $newRooms = []; + $allRooms = $this->getAllRooms(); + + foreach ($scheduleRooms as $room) { + if ($allRooms->where('name', $room->getName())->count()) { + continue; + } + + $newRooms[] = $room; + } + + return $newRooms; + } + + /** + * @param Schedule $schedule + * @param ScheduleUrl $scheduleUrl + * @param int $shiftType + * @param int $minutesBefore + * @param int $minutesAfter + * @return Event[] + */ + protected function shiftsDiff( + Schedule $schedule, + ScheduleUrl $scheduleUrl, + int $shiftType, + int $minutesBefore, + int $minutesAfter + ): array { + /** @var Event[] $newEvents */ + $newEvents = []; + /** @var Event[] $changeEvents */ + $changeEvents = []; + /** @var Event[] $scheduleEvents */ + $scheduleEvents = []; + /** @var Event[] $deleteEvents */ + $deleteEvents = []; + $rooms = $this->getAllRooms(); + + foreach ($schedule->getDay() as $day) { + foreach ($day->getRoom() as $room) { + foreach ($room->getEvent() as $event) { + $scheduleEvents[$event->getGuid()] = $event; + + $event->getDate()->subMinutes($minutesBefore); + $event->getEndDate()->addMinutes($minutesAfter); + } + } + } + + $scheduleEventsGuidList = array_keys($scheduleEvents); + $existingShifts = $this->getScheduleShiftsByGuid($scheduleUrl, $scheduleEventsGuidList); + foreach ($existingShifts as $shift) { + $guid = $shift->guid; + $shift = $this->loadShift($shift->shift_id); + $event = $scheduleEvents[$guid]; + + if ( + $shift->title != $event->getTitle() + || $shift->shift_type_id != $shiftType + || Carbon::createFromTimestamp($shift->start) != $event->getDate() + || Carbon::createFromTimestamp($shift->end) != $event->getEndDate() + || $shift->room_id != $rooms->where('name', $event->getRoom()->getName())->first()->id + || $shift->url != $event->getUrl() + ) { + $changeEvents[$guid] = $event; + } + + unset($scheduleEvents[$guid]); + } + + foreach ($scheduleEvents as $scheduleEvent) { + $newEvents[$scheduleEvent->getGuid()] = $scheduleEvent; + } + + $scheduleShifts = $this->getScheduleShiftsWhereNotGuid($scheduleUrl, $scheduleEventsGuidList); + foreach ($scheduleShifts as $shift) { + $event = $this->eventFromScheduleShift($shift); + $deleteEvents[$event->getGuid()] = $event; + } + + return [$newEvents, $changeEvents, $deleteEvents]; + } + + /** + * @param ScheduleShift $scheduleShift + * @return Event + */ + protected function eventFromScheduleShift(ScheduleShift $scheduleShift): Event + { + $shift = $this->loadShift($scheduleShift->shift_id); + $start = Carbon::createFromTimestamp($shift->start); + $end = Carbon::createFromTimestamp($shift->end); + $duration = $start->diff($end); + + $event = new Event( + $scheduleShift->guid, + 0, + new Room($shift->room_name), + $shift->title, + '', + 'n/a', + Carbon::createFromTimestamp($shift->start), + $start->format('H:i'), + $duration->format('%H:%I'), + '', + '', + '' + ); + + return $event; + } + + /** + * @return Collection + */ + protected function getAllRooms(): Collection + { + return new Collection($this->db->select('SELECT RID as id, Name as name FROM Room')); + } + + /** + * @param ScheduleUrl $scheduleUrl + * @param string[] $events + * @return QueryBuilder[]|DatabaseCollection|ScheduleShift[] + */ + protected function getScheduleShiftsByGuid(ScheduleUrl $scheduleUrl, array $events) + { + return ScheduleShift::query() + ->whereIn('guid', $events) + ->where('schedule_id', $scheduleUrl->id) + ->get(); + } + + /** + * @param ScheduleUrl $scheduleUrl + * @param string[] $events + * @return QueryBuilder[]|DatabaseCollection|ScheduleShift[] + */ + protected function getScheduleShiftsWhereNotGuid(ScheduleUrl $scheduleUrl, array $events) + { + return ScheduleShift::query() + ->whereNotIn('guid', $events) + ->where('schedule_id', $scheduleUrl->id) + ->get(); + } + + /** + * @param $id + * @return stdClass|null + */ + protected function loadShift($id): ?stdClass + { + return $this->db->selectOne( + ' + SELECT + s.SID AS id, + s.title, + s.start, + s.end, + s.shifttype_id AS shift_type_id, + s.RID AS room_id, + r.Name AS room_name, + s.URL as url + FROM Shifts AS s + LEFT JOIN Room r on s.RID = r.RID + WHERE SID = ? + ', + [$id] + ); + } + + /** + * @return string[] + */ + protected function getShiftTypes() + { + $return = []; + /** @var stdClass[] $shiftTypes */ + $shiftTypes = $this->db->select('SELECT t.id, t.name FROM ShiftTypes AS t'); + + foreach ($shiftTypes as $shiftType) { + $return[$shiftType->id] = $shiftType->name; + } + + 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 + */ + protected function log(string $message, array $context = []): void + { + $user = auth()->user(); + $message = sprintf('%s (%u): %s', $user->name, $user->id, $message); + + $this->log->info($message, $context); + } +} -- cgit v1.3.1 From 47e209e12594b935ba1670096ae4c55538ebde94 Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sat, 30 Nov 2019 01:15:39 +0100 Subject: ImportSchedule: Append talk language to shift title Resolves #587 (Include talk language into Frab-imported shift titles) --- includes/pages/schedule/ImportSchedule.php | 1 + src/Helpers/Schedule/Event.php | 8 ++++++++ tests/Unit/Helpers/Schedule/EventTest.php | 4 ++++ 3 files changed, 13 insertions(+) (limited to 'includes/pages/schedule/ImportSchedule.php') diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php index 1b03b57b..8bc769d4 100644 --- a/includes/pages/schedule/ImportSchedule.php +++ b/includes/pages/schedule/ImportSchedule.php @@ -440,6 +440,7 @@ class ImportSchedule extends BaseController $event->getDate()->subMinutes($minutesBefore); $event->getEndDate()->addMinutes($minutesAfter); + $event->setTitle(sprintf('%s [%s]', $event->getTitle(), $event->getLanguage())); } } } diff --git a/src/Helpers/Schedule/Event.php b/src/Helpers/Schedule/Event.php index 46970e7b..ab3b39a5 100644 --- a/src/Helpers/Schedule/Event.php +++ b/src/Helpers/Schedule/Event.php @@ -183,6 +183,14 @@ class Event return $this->title; } + /** + * @param string $title + */ + public function setTitle(string $title): void + { + $this->title = $title; + } + /** * @return string */ diff --git a/tests/Unit/Helpers/Schedule/EventTest.php b/tests/Unit/Helpers/Schedule/EventTest.php index 262aeea7..d9706b13 100644 --- a/tests/Unit/Helpers/Schedule/EventTest.php +++ b/tests/Unit/Helpers/Schedule/EventTest.php @@ -84,6 +84,7 @@ class EventTest extends TestCase * @covers \Engelsystem\Helpers\Schedule\Event::getId * @covers \Engelsystem\Helpers\Schedule\Event::getRoom * @covers \Engelsystem\Helpers\Schedule\Event::getTitle + * @covers \Engelsystem\Helpers\Schedule\Event::setTitle * @covers \Engelsystem\Helpers\Schedule\Event::getSubtitle * @covers \Engelsystem\Helpers\Schedule\Event::getType * @covers \Engelsystem\Helpers\Schedule\Event::getDate @@ -141,5 +142,8 @@ class EventTest extends TestCase $this->assertEquals($attachments, $event->getAttachments()); $this->assertEquals('https://foo.bar/2-lorem', $event->getUrl()); $this->assertEquals('https://videos.orem.ipsum/2-lorem.mp4', $event->getVideoDownloadUrl()); + + $event->setTitle('Event title'); + $this->assertEquals('Event title', $event->getTitle()); } } -- cgit v1.3.1 From 064a1750279be244f86cf24f29749b391b11f1dd Mon Sep 17 00:00:00 2001 From: Igor Scheller Date: Sun, 1 Dec 2019 22:10:10 +0100 Subject: Renamed GuzzleServiceProvider to HttpClientServiceProvider, fixed translation and import error --- config/app.php | 2 +- includes/pages/schedule/ImportSchedule.php | 3 ++- resources/lang/de_DE/default.po | 2 +- src/Http/GuzzleServiceProvider.php | 25 ------------------- src/Http/HttpClientServiceProvider.php | 25 +++++++++++++++++++ tests/Unit/Http/GuzzleServiceProviderTest.php | 29 ----------------------- tests/Unit/Http/HttpClientServiceProviderTest.php | 29 +++++++++++++++++++++++ 7 files changed, 58 insertions(+), 57 deletions(-) delete mode 100644 src/Http/GuzzleServiceProvider.php create mode 100644 src/Http/HttpClientServiceProvider.php delete mode 100644 tests/Unit/Http/GuzzleServiceProviderTest.php create mode 100644 tests/Unit/Http/HttpClientServiceProviderTest.php (limited to 'includes/pages/schedule/ImportSchedule.php') diff --git a/config/app.php b/config/app.php index e1001900..8ede567e 100644 --- a/config/app.php +++ b/config/app.php @@ -31,7 +31,7 @@ return [ // Additional services \Engelsystem\Helpers\VersionServiceProvider::class, \Engelsystem\Mail\MailerServiceProvider::class, - \Engelsystem\Http\GuzzleServiceProvider::class, + \Engelsystem\Http\HttpClientServiceProvider::class, ], // Application middleware diff --git a/includes/pages/schedule/ImportSchedule.php b/includes/pages/schedule/ImportSchedule.php index 8bc769d4..136f1816 100644 --- a/includes/pages/schedule/ImportSchedule.php +++ b/includes/pages/schedule/ImportSchedule.php @@ -451,13 +451,14 @@ class ImportSchedule extends BaseController $guid = $shift->guid; $shift = $this->loadShift($shift->shift_id); $event = $scheduleEvents[$guid]; + $room = $rooms->where('name', $event->getRoom()->getName())->first(); if ( $shift->title != $event->getTitle() || $shift->shift_type_id != $shiftType || Carbon::createFromTimestamp($shift->start) != $event->getDate() || Carbon::createFromTimestamp($shift->end) != $event->getEndDate() - || $shift->room_id != $rooms->where('name', $event->getRoom()->getName())->first()->id + || $shift->room_id != ($room->id ?? '') || $shift->url != $event->getUrl() ) { $changeEvents[$guid] = $event; diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index d5f30f22..7f017f13 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2817,7 +2817,7 @@ msgid "schedule.import.title" msgstr "Programm importieren" msgid "schedule.import.text" -msgstr "Dieser Import erstellt Räume and erstellt, aktualisiert und löscht Schichten anhand des schedule.xml exportes." +msgstr "Dieser Import erstellt Räume und erstellt, aktualisiert und löscht Schichten anhand des schedule.xml exportes." msgid "schedule.import.load.title" msgstr "Programm importieren: Vorschau" diff --git a/src/Http/GuzzleServiceProvider.php b/src/Http/GuzzleServiceProvider.php deleted file mode 100644 index f81a91f5..00000000 --- a/src/Http/GuzzleServiceProvider.php +++ /dev/null @@ -1,25 +0,0 @@ -app->when(GuzzleClient::class) - ->needs('$config') - ->give( - function () { - return [ - // No exception on >= 400 responses - 'http_errors' => false, - // Wait max n seconds for a response - 'timeout' => 2.0, - ]; - } - ); - } -} diff --git a/src/Http/HttpClientServiceProvider.php b/src/Http/HttpClientServiceProvider.php new file mode 100644 index 00000000..113af713 --- /dev/null +++ b/src/Http/HttpClientServiceProvider.php @@ -0,0 +1,25 @@ +app->when(GuzzleClient::class) + ->needs('$config') + ->give( + function () { + return [ + // No exception on >= 400 responses + 'http_errors' => false, + // Wait max n seconds for a response + 'timeout' => 2.0, + ]; + } + ); + } +} diff --git a/tests/Unit/Http/GuzzleServiceProviderTest.php b/tests/Unit/Http/GuzzleServiceProviderTest.php deleted file mode 100644 index 4d3c938b..00000000 --- a/tests/Unit/Http/GuzzleServiceProviderTest.php +++ /dev/null @@ -1,29 +0,0 @@ -register(); - - /** @var GuzzleClient $guzzle */ - $guzzle = $app->make(GuzzleClient::class); - $config = $guzzle->getConfig(); - - $this->assertFalse($config['http_errors']); - $this->assertArrayHasKey('timeout', $config); - } -} diff --git a/tests/Unit/Http/HttpClientServiceProviderTest.php b/tests/Unit/Http/HttpClientServiceProviderTest.php new file mode 100644 index 00000000..7dfc0a3e --- /dev/null +++ b/tests/Unit/Http/HttpClientServiceProviderTest.php @@ -0,0 +1,29 @@ +register(); + + /** @var GuzzleClient $guzzle */ + $guzzle = $app->make(GuzzleClient::class); + $config = $guzzle->getConfig(); + + $this->assertFalse($config['http_errors']); + $this->assertArrayHasKey('timeout', $config); + } +} -- cgit v1.3.1