Added Schedule parsing and replaced old Fahrplan importer

Resolves #553 (Change Frab Import from xCal to XML)
Resolves #538 (Feature Request: Multi Frab Import)
main
Igor Scheller 5 years ago
parent 377b390c97
commit 42721e9572

@ -65,7 +65,7 @@ The following instructions explain how to get, build and run the latest engelsys
``` ```
### Configuration and Setup ### Configuration and Setup
* The webserver must have write access to the ```import``` and ```storage``` directories and read access for all other directories * The webserver must have write access to the ```storage``` directory and read access for all other directories
* The webserver must point to the ```public``` directory. * The webserver must point to the ```public``` directory.
* The webserver must read the ```.htaccess``` file and ```mod_rewrite``` must be enabled * The webserver must read the ```.htaccess``` file and ```mod_rewrite``` must be enabled

@ -28,6 +28,7 @@
"doctrine/dbal": "^2.9", "doctrine/dbal": "^2.9",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"gettext/gettext": "^4.6", "gettext/gettext": "^4.6",
"guzzlehttp/guzzle": "^6.3",
"illuminate/container": "5.8.*", "illuminate/container": "5.8.*",
"illuminate/database": "5.8.*", "illuminate/database": "5.8.*",
"illuminate/support": "5.8.*", "illuminate/support": "5.8.*",

@ -31,6 +31,7 @@ return [
// Additional services // Additional services
\Engelsystem\Helpers\VersionServiceProvider::class, \Engelsystem\Helpers\VersionServiceProvider::class,
\Engelsystem\Mail\MailerServiceProvider::class, \Engelsystem\Mail\MailerServiceProvider::class,
\Engelsystem\Http\GuzzleServiceProvider::class,
], ],
// Application middleware // Application middleware

@ -25,3 +25,19 @@ $route->get('/stats', 'Metrics\\Controller@stats');
// API // API
$route->get('/api[/{resource:.+}]', 'ApiController@index'); $route->get('/api[/{resource:.+}]', 'ApiController@index');
// Administration
$route->addGroup(
'/admin',
function (RouteCollector $route) {
// Schedule
$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');
}
);
}
);

@ -0,0 +1,48 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
class MigrateAdminSchedulePermissions extends Migration
{
/**
* Run the migration
*/
public function up()
{
if (!$this->schema->hasTable('Privileges')) {
return;
}
$this->schema->getConnection()
->table('Privileges')
->where('name', 'admin_import')
->update(
[
'name' => 'schedule.import',
'desc' => 'Import rooms and shifts from schedule.xml',
]
);
}
/**
* Reverse the migration
*/
public function down()
{
if (!$this->schema->hasTable('Privileges')) {
return;
}
$this->schema->getConnection()
->table('Privileges')
->where('name', 'schedule.import')
->update(
[
'name' => 'admin_import',
'desc' => 'Import rooms and shifts from schedule.xcs/schedule.xcal',
]
);
}
}

@ -0,0 +1,90 @@
<?php
namespace Engelsystem\Migrations;
use Engelsystem\Database\Migration\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateScheduleShiftTable extends Migration
{
use Reference;
/**
* Run the migration
*/
public function up()
{
$this->schema->create(
'schedules',
function (Blueprint $table) {
$table->increments('id');
$table->string('url');
}
);
$this->schema->create(
'schedule_shift',
function (Blueprint $table) {
$table->integer('shift_id')->index()->unique();
if ($this->schema->hasTable('Shifts')) {
// Legacy table access
$table->foreign('shift_id')
->references('SID')->on('Shifts')
->onUpdate('cascade')
->onDelete('cascade');
}
$this->references($table, 'schedules');
$table->uuid('guid');
}
);
if ($this->schema->hasTable('Shifts')) {
$this->schema->table(
'Shifts',
function (Blueprint $table) {
$table->dropColumn('PSID');
}
);
}
if ($this->schema->hasTable('Room')) {
$this->schema->table(
'Room',
function (Blueprint $table) {
$table->dropColumn('from_frab');
}
);
}
}
/**
* Reverse the migration
*/
public function down()
{
if ($this->schema->hasTable('Room')) {
$this->schema->table(
'Room',
function (Blueprint $table) {
$table->boolean('from_frab')
->default(false);
}
);
}
if ($this->schema->hasTable('Shifts')) {
$this->schema->table(
'Shifts',
function (Blueprint $table) {
$table->integer('PSID')
->nullable()->default(null)
->unique();
}
);
}
$this->schema->drop('schedule_shift');
$this->schema->drop('schedules');
}
}

@ -4,6 +4,7 @@ namespace Engelsystem\Migrations;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\ColumnDefinition; use Illuminate\Database\Schema\ColumnDefinition;
use Illuminate\Support\Str;
trait Reference trait Reference
{ {
@ -11,20 +12,25 @@ trait Reference
* @param Blueprint $table * @param Blueprint $table
* @param bool $setPrimary * @param bool $setPrimary
*/ */
protected function referencesUser(Blueprint $table, $setPrimary = false) protected function referencesUser(Blueprint $table, bool $setPrimary = false)
{ {
$this->references($table, 'users', 'user_id', $setPrimary); $this->references($table, 'users', null, $setPrimary);
} }
/** /**
* @param Blueprint $table * @param Blueprint $table
* @param string $targetTable * @param string $targetTable
* @param string $fromColumn * @param string|null $fromColumn
* @param bool $setPrimary * @param bool $setPrimary
* @return ColumnDefinition * @return ColumnDefinition
*/ */
protected function references(Blueprint $table, $targetTable, $fromColumn, $setPrimary = false): ColumnDefinition protected function references(
{ Blueprint $table,
string $targetTable,
?string $fromColumn = null,
bool $setPrimary = false
): ColumnDefinition {
$fromColumn = $fromColumn ?? Str::singular($targetTable) . '_id';
$col = $table->unsignedInteger($fromColumn); $col = $table->unsignedInteger($fromColumn);
if ($setPrimary) { if ($setPrimary) {

@ -15,7 +15,6 @@ COPY .babelrc .browserslistrc composer.json LICENSE package.json README.md webpa
COPY bin/ /app/bin COPY bin/ /app/bin
COPY config/ /app/config COPY config/ /app/config
COPY db/ /app/db COPY db/ /app/db
RUN mkdir /app/import/
COPY includes/ /app/includes COPY includes/ /app/includes
COPY public/ /app/public COPY public/ /app/public
COPY resources/views /app/resources/views COPY resources/views /app/resources/views
@ -35,7 +34,7 @@ WORKDIR /var/www
RUN apk add --no-cache icu-dev && \ RUN apk add --no-cache icu-dev && \
docker-php-ext-install intl pdo_mysql docker-php-ext-install intl pdo_mysql
COPY --from=data /app/ /var/www COPY --from=data /app/ /var/www
RUN chown -R www-data:www-data /var/www/import/ /var/www/storage/ && \ RUN chown -R www-data:www-data /var/www/storage/ && \
rm -r /var/www/html rm -r /var/www/html
ARG VERSION ARG VERSION

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -69,7 +69,6 @@ $includeFiles = [
__DIR__ . '/../includes/pages/admin_arrive.php', __DIR__ . '/../includes/pages/admin_arrive.php',
__DIR__ . '/../includes/pages/admin_free.php', __DIR__ . '/../includes/pages/admin_free.php',
__DIR__ . '/../includes/pages/admin_groups.php', __DIR__ . '/../includes/pages/admin_groups.php',
__DIR__ . '/../includes/pages/admin_import.php',
__DIR__ . '/../includes/pages/admin_log.php', __DIR__ . '/../includes/pages/admin_log.php',
__DIR__ . '/../includes/pages/admin_questions.php', __DIR__ . '/../includes/pages/admin_questions.php',
__DIR__ . '/../includes/pages/admin_rooms.php', __DIR__ . '/../includes/pages/admin_rooms.php',
@ -82,6 +81,8 @@ $includeFiles = [
__DIR__ . '/../includes/pages/user_questions.php', __DIR__ . '/../includes/pages/user_questions.php',
__DIR__ . '/../includes/pages/user_settings.php', __DIR__ . '/../includes/pages/user_settings.php',
__DIR__ . '/../includes/pages/user_shifts.php', __DIR__ . '/../includes/pages/user_shifts.php',
__DIR__ . '/../includes/pages/schedule/ImportSchedule.php',
]; ];
foreach ($includeFiles as $file) { foreach ($includeFiles as $file) {

@ -61,36 +61,21 @@ function Room_delete($room_id)
engelsystem_log('Room deleted: ' . $room['Name']); engelsystem_log('Room deleted: ' . $room['Name']);
} }
/**
* Delete a room by its name
*
* @param string $name
*/
function Room_delete_by_name($name)
{
DB::delete('DELETE FROM `Room` WHERE `Name` = ?', [
$name
]);
engelsystem_log('Room deleted: ' . $name);
}
/** /**
* Create a new room * Create a new room
* *
* @param string $name Name of the room * @param string $name Name of the room
* @param boolean $from_frab Is this a frab imported room?
* @param string $map_url URL to a map tha can be displayed in an iframe * @param string $map_url URL to a map tha can be displayed in an iframe
* @param string description Markdown description * @param string description Markdown description
* @return false|int * @return false|int
*/ */
function Room_create($name, $from_frab, $map_url, $description) function Room_create($name, $map_url, $description)
{ {
DB::insert(' DB::insert('
INSERT INTO `Room` (`Name`, `from_frab`, `map_url`, `description`) INSERT INTO `Room` (`Name`, `map_url`, `description`)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?)
', [ ', [
$name, $name,
(int)$from_frab,
$map_url, $map_url,
$description $description
]); ]);
@ -98,7 +83,6 @@ function Room_create($name, $from_frab, $map_url, $description)
engelsystem_log( engelsystem_log(
'Room created: ' . $name 'Room created: ' . $name
. ', frab import: ' . ($from_frab ? 'Yes' : '')
. ', map_url: ' . $map_url . ', map_url: ' . $map_url
. ', description: ' . $description . ', description: ' . $description
); );
@ -107,28 +91,25 @@ function Room_create($name, $from_frab, $map_url, $description)
} }
/** /**
* update a room * Update a room
* *
* @param int $room_id The rooms id * @param int $room_id The rooms id
* @param string $name Name of the room * @param string $name Name of the room
* @param boolean $from_frab Is this a frab imported room?
* @param string $map_url URL to a map tha can be displayed in an iframe * @param string $map_url URL to a map tha can be displayed in an iframe
* @param string $description Markdown description * @param string $description Markdown description
* @return int * @return int
*/ */
function Room_update($room_id, $name, $from_frab, $map_url, $description) function Room_update($room_id, $name, $map_url, $description)
{ {
$result = DB::update(' $result = DB::update('
UPDATE `Room` UPDATE `Room`
SET SET
`Name`=?, `Name`=?,
`from_frab`=?,
`map_url`=?, `map_url`=?,
`description`=? `description`=?
WHERE `RID`=? WHERE `RID`=?
LIMIT 1', [ LIMIT 1', [
$name, $name,
(int)$from_frab,
$map_url, $map_url,
$description, $description,
$room_id $room_id
@ -136,7 +117,6 @@ function Room_update($room_id, $name, $from_frab, $map_url, $description)
engelsystem_log( engelsystem_log(
'Room updated: ' . $name . 'Room updated: ' . $name .
', frab import: ' . ($from_frab ? 'Yes' : '') .
', map_url: ' . $map_url . ', map_url: ' . $map_url .
', description: ' . $description ', description: ' . $description
); );

@ -14,17 +14,19 @@ function Shifts_by_angeltype($angeltype)
return DB::select(' return DB::select('
SELECT DISTINCT `Shifts`.* FROM `Shifts` SELECT DISTINCT `Shifts`.* FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id` = `Shifts`.`SID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id` = `Shifts`.`SID`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `NeededAngelTypes`.`angel_type_id` = ? WHERE `NeededAngelTypes`.`angel_type_id` = ?
AND `NeededAngelTypes`.`count` > 0 AND `NeededAngelTypes`.`count` > 0
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION UNION
SELECT DISTINCT `Shifts`.* FROM `Shifts` SELECT DISTINCT `Shifts`.* FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id` = `Shifts`.`RID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id` = `Shifts`.`RID`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `NeededAngelTypes`.`angel_type_id` = ? WHERE `NeededAngelTypes`.`angel_type_id` = ?
AND `NeededAngelTypes`.`count` > 0 AND `NeededAngelTypes`.`count` > 0
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
', [$angeltype['id'], $angeltype['id']]); ', [$angeltype['id'], $angeltype['id']]);
} }
@ -41,19 +43,21 @@ function Shifts_free($start, $end)
SELECT * FROM ( SELECT * FROM (
SELECT * SELECT *
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE (`end` > ? AND `start` < ?) WHERE (`end` > ? AND `start` < ?)
AND (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`) AND (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`)
> (SELECT COUNT(*) FROM `ShiftEntry` WHERE `ShiftEntry`.`SID`=`Shifts`.`SID` AND `freeloaded`=0) > (SELECT COUNT(*) FROM `ShiftEntry` WHERE `ShiftEntry`.`SID`=`Shifts`.`SID` AND `freeloaded`=0)
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION UNION
SELECT * SELECT *
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE (`end` > ? AND `start` < ?) WHERE (`end` > ? AND `start` < ?)
AND (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`room_id`=`Shifts`.`RID`) AND (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`room_id`=`Shifts`.`RID`)
> (SELECT COUNT(*) FROM `ShiftEntry` WHERE `ShiftEntry`.`SID`=`Shifts`.`SID` AND `freeloaded`=0) > (SELECT COUNT(*) FROM `ShiftEntry` WHERE `ShiftEntry`.`SID`=`Shifts`.`SID` AND `freeloaded`=0)
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
) AS `tmp` ) AS `tmp`
ORDER BY `tmp`.`start` ORDER BY `tmp`.`start`
", [ ", [
@ -69,16 +73,6 @@ function Shifts_free($start, $end)
return $free_shifts; return $free_shifts;
} }
/**
* Returns all shifts with a PSID (from frab import)
*
* @return array[]
*/
function Shifts_from_frab()
{
return DB::select('SELECT * FROM `Shifts` WHERE `PSID` IS NOT NULL ORDER BY `start`');
}
/** /**
* @param array|int $room * @param array|int $room
* @return array[] * @return array[]
@ -103,11 +97,12 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
JOIN `Room` USING (`RID`) JOIN `Room` USING (`RID`)
JOIN `ShiftTypes` ON `ShiftTypes`.`id` = `Shifts`.`shifttype_id` JOIN `ShiftTypes` ON `ShiftTypes`.`id` = `Shifts`.`shifttype_id`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id` = `Shifts`.`SID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id` = `Shifts`.`SID`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ') WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ')
AND `start` BETWEEN ? AND ? AND `start` BETWEEN ? AND ?
AND `NeededAngelTypes`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ') AND `NeededAngelTypes`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ')
AND `NeededAngelTypes`.`count` > 0 AND `NeededAngelTypes`.`count` > 0
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION UNION
@ -116,11 +111,12 @@ function Shifts_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
JOIN `Room` USING (`RID`) JOIN `Room` USING (`RID`)
JOIN `ShiftTypes` ON `ShiftTypes`.`id` = `Shifts`.`shifttype_id` JOIN `ShiftTypes` ON `ShiftTypes`.`id` = `Shifts`.`shifttype_id`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ') WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ')
AND `start` BETWEEN ? AND ? AND `start` BETWEEN ? AND ?
AND `NeededAngelTypes`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ') AND `NeededAngelTypes`.`angel_type_id` IN (' . implode(',', $shiftsFilter->getTypes()) . ')
AND `NeededAngelTypes`.`count` > 0 AND `NeededAngelTypes`.`count` > 0
AND NOT `Shifts`.`PSID` IS NULL) AS tmp_shifts AND NOT s.shift_id IS NULL) AS tmp_shifts
ORDER BY `room_name`, `start`'; ORDER BY `room_name`, `start`';
@ -152,9 +148,10 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
FROM `Shifts` FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id`=`Shifts`.`SID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`
JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id` JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ') WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ')
AND `start` BETWEEN ? AND ? AND `start` BETWEEN ? AND ?
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION UNION
@ -168,9 +165,10 @@ function NeededAngeltypes_by_ShiftsFilter(ShiftsFilter $shiftsFilter)
FROM `Shifts` FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID`
JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id` JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ') WHERE `Shifts`.`RID` IN (' . implode(',', $shiftsFilter->getRooms()) . ')
AND `start` BETWEEN ? AND ? AND `start` BETWEEN ? AND ?
AND NOT `Shifts`.`PSID` IS NULL'; AND NOT s.shift_id IS NULL';
return DB::select( return DB::select(
$sql, $sql,
@ -201,9 +199,10 @@ function NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype)
FROM `Shifts` FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id`=`Shifts`.`SID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`
JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id` JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`SID`=? WHERE `Shifts`.`SID`=?
AND `AngelTypes`.`id`=? AND `AngelTypes`.`id`=?
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION UNION
@ -217,9 +216,10 @@ function NeededAngeltype_by_Shift_and_Angeltype($shift, $angeltype)
FROM `Shifts` FROM `Shifts`
JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID` JOIN `NeededAngelTypes` ON `NeededAngelTypes`.`room_id`=`Shifts`.`RID`
JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id` JOIN `AngelTypes` ON `AngelTypes`.`id`= `NeededAngelTypes`.`angel_type_id`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `Shifts`.`SID`=? WHERE `Shifts`.`SID`=?
AND `AngelTypes`.`id`=? AND `AngelTypes`.`id`=?
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
', ',
[ [
$shift['SID'], $shift['SID'],
@ -494,16 +494,6 @@ function Shift_signup_allowed(
); );
} }
/**
* Delete a shift by its external id.
*
* @param int $shift_psid
*/
function Shift_delete_by_psid($shift_psid)
{
DB::delete('DELETE FROM `Shifts` WHERE `PSID`=?', [$shift_psid]);
}
/** /**
* Delete a shift. * Delete a shift.
* *
@ -535,7 +525,6 @@ function Shift_update($shift)
`RID` = ?, `RID` = ?,
`title` = ?, `title` = ?,
`URL` = ?, `URL` = ?,
`PSID` = ?,
`edited_by_user_id` = ?, `edited_by_user_id` = ?,
`edited_at_timestamp` = ? `edited_at_timestamp` = ?
WHERE `SID` = ? WHERE `SID` = ?
@ -547,7 +536,6 @@ function Shift_update($shift)
$shift['RID'], $shift['RID'],
$shift['title'], $shift['title'],
$shift['URL'], $shift['URL'],
$shift['PSID'],
$user->id, $user->id,
time(), time(),
$shift['SID'] $shift['SID']
@ -555,25 +543,6 @@ function Shift_update($shift)
); );
} }
/**
* Update a shift by its external id.
*
* @param array $shift
* @return int
* @throws Exception
*/
function Shift_update_by_psid($shift)
{
$shift_source = DB::selectOne('SELECT `SID` FROM `Shifts` WHERE `PSID`=?', [$shift['PSID']]);
if (empty($shift_source)) {
throw new Exception('Shift not found.');
}
$shift['SID'] = $shift_source['SID'];
return Shift_update($shift);
}
/** /**
* Create a new shift. * Create a new shift.
* *
@ -590,12 +559,11 @@ function Shift_create($shift)
`RID`, `RID`,
`title`, `title`,
`URL`, `URL`,
`PSID`,
`created_by_user_id`, `created_by_user_id`,
`edited_at_timestamp`, `edited_at_timestamp`,
`created_at_timestamp` `created_at_timestamp`
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
', ',
[ [
$shift['shifttype_id'], $shift['shifttype_id'],
@ -604,7 +572,6 @@ function Shift_create($shift)
$shift['RID'], $shift['RID'],
$shift['title'], $shift['title'],
$shift['URL'], $shift['URL'],
$shift['PSID'],
auth()->user()->id, auth()->user()->id,
time(), time(),
time(), time(),

@ -39,8 +39,9 @@ function stats_hours_to_work()
(SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`) (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`shift_id`=`Shifts`.`SID`)
* (`Shifts`.`end` - `Shifts`.`start`)/3600 AS `count` * (`Shifts`.`end` - `Shifts`.`start`)/3600 AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` >= ? WHERE `end` >= ?
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION ALL UNION ALL
@ -48,8 +49,9 @@ function stats_hours_to_work()
(SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`room_id`=`Shifts`.`RID`) (SELECT SUM(`count`) FROM `NeededAngelTypes` WHERE `NeededAngelTypes`.`room_id`=`Shifts`.`RID`)
* (`Shifts`.`end` - `Shifts`.`start`)/3600 AS `count` * (`Shifts`.`end` - `Shifts`.`start`)/3600 AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` >= ? WHERE `end` >= ?
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
) AS `tmp` ) AS `tmp`
", [ ", [
time(), time(),
@ -90,8 +92,9 @@ function stats_angels_needed_three_hours()
) )
AS `count` AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` > ? AND `start` < ? WHERE `end` > ? AND `start` < ?
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION ALL UNION ALL
@ -113,8 +116,9 @@ function stats_angels_needed_three_hours()
) )
AS `count` AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` > ? AND `start` < ? WHERE `end` > ? AND `start` < ?
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
) AS `tmp`", [ ) AS `tmp`", [
$now, $now,
$in3hours, $in3hours,
@ -163,8 +167,9 @@ function stats_angels_needed_for_nightshifts()
) )
AS `count` AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` > ? AND `start` < ? WHERE `end` > ? AND `start` < ?
AND `Shifts`.`PSID` IS NULL AND s.shift_id IS NULL
UNION ALL UNION ALL
@ -186,8 +191,9 @@ function stats_angels_needed_for_nightshifts()
) )
AS `count` AS `count`
FROM `Shifts` FROM `Shifts`
LEFT JOIN schedule_shift AS s on Shifts.SID = s.shift_id
WHERE `end` > ? AND `start` < ? WHERE `end` > ? AND `start` < ?
AND NOT `Shifts`.`PSID` IS NULL AND NOT s.shift_id IS NULL
) AS `tmp`", [ ) AS `tmp`", [
$night_start, $night_start,
$night_end, $night_end,

@ -1,478 +0,0 @@
<?php
/**
* @return string
*/
function admin_import_title()
{
return __('Frab import');
}
/**
* @return string
*/
function admin_import()
{
global $rooms_import;
$user = auth()->user();
$html = '';
$import_dir = __DIR__ . '/../../import';
$request = request();
$step = 'input';
if (
$request->has('step')
&& in_array($request->input('step'), [
'input',
'check',
'import'
])
) {
$step = $request->input('step');
}
try {
$test_handle = @fopen($import_dir . '/tmp', 'w');
fclose($test_handle);
@unlink($import_dir . '/tmp');
} catch (Exception $e) {
error(__('Webserver has no write-permission on import directory.'));
}
$import_file = $import_dir . '/import_' . $user->id . '.xml';
$shifttype_id = null;
$add_minutes_start = 15;
$add_minutes_end = 15;
$shifttypes_source = ShiftTypes();
$shifttypes = [];
foreach ($shifttypes_source as $shifttype) {
$shifttypes[$shifttype['id']] = $shifttype['name'];
}
switch ($step) {
case 'input':
$valid = false;
if ($request->hasPostData('submit')) {
$valid = true;
if ($request->has('shifttype_id') && isset($shifttypes[$request->input('shifttype_id')])) {
$shifttype_id = $request->input('shifttype_id');
} else {
$valid = false;
error(__('Please select a shift type.'));
}
$minutes_start = trim($request->input('add_minutes_start'));
if ($request->has('add_minutes_start') && is_numeric($minutes_start)) {
$add_minutes_start = $minutes_start;
} else {
$valid = false;
error(__('Please enter an amount of minutes to add to a talk\'s begin.'));
}
if ($request->has('add_minutes_end') && is_numeric(trim($request->input('add_minutes_end')))) {
$add_minutes_end = trim($request->input('add_minutes_end'));
} else {
$valid = false;
error(__('Please enter an amount of minutes to add to a talk\'s end.'));
}
if (isset($_FILES['xcal_file']) && ($_FILES['xcal_file']['error'] == 0)) {
if (move_uploaded_file($_FILES['xcal_file']['tmp_name'], $import_file)) {
libxml_use_internal_errors(true);
if (simplexml_load_file($import_file) === false) {
$valid = false;
error(__('No valid xml/xcal file provided.'));
unlink($import_file);
}
} else {
$valid = false;
error(__('File upload went wrong.'));
}
} else {
$valid = false;
error(__('Please provide some data.'));
}
}
if ($valid) {
throw_redirect(
page_link_to('admin_import', [
'step' => 'check',
'shifttype_id' => $shifttype_id,
'add_minutes_end' => $add_minutes_end,
'add_minutes_start' => $add_minutes_start,
])
);
} else {
$html .= div('well well-sm text-center', [
__('File Upload')
. mute(glyph('arrow-right'))
. mute(__('Validation'))
. mute(glyph('arrow-right'))
. mute(__('Import'))
]) . div('row', [
div('col-md-offset-3 col-md-6', [
form([
form_info(
'',
__('This import will create/update/delete rooms and shifts by given FRAB-export file. The needed file format is xcal.')
),
form_select('shifttype_id', __('Shifttype'), $shifttypes, $shifttype_id),
form_spinner('add_minutes_start', __('Add minutes to start'), $add_minutes_start),
form_spinner('add_minutes_end', __('Add minutes to end'), $add_minutes_end),
form_file('xcal_file', __('xcal-File (.xcal)')),
form_submit('submit', __('Import'))
])
])
]);
}
break;
case 'check':
if (!file_exists($import_file)) {
error(__('Missing import file.'));
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('shifttype_id') && isset($shifttypes[$request->input('shifttype_id')])) {
$shifttype_id = $request->input('shifttype_id');
} else {
error(__('Please select a shift type.'));
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('add_minutes_start') && is_numeric(trim($request->input('add_minutes_start')))) {
$add_minutes_start = trim($request->input('add_minutes_start'));
} else {
error(__('Please enter an amount of minutes to add to a talk\'s begin.'));
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('add_minutes_end') && is_numeric(trim($request->input(('add_minutes_end'))))) {
$add_minutes_end = trim($request->input('add_minutes_end'));
} else {
error(__('Please enter an amount of minutes to add to a talk\'s end.'));
throw_redirect(page_link_to('admin_import'));
}
list($rooms_new, $rooms_deleted) = prepare_rooms($import_file);
list($events_new, $events_updated, $events_deleted) = prepare_events(
$import_file,
$shifttype_id,
$add_minutes_start,
$add_minutes_end
);
$html .= div(
'well well-sm text-center',
[
'<span class="text-success">' . __('File Upload') . glyph('ok-circle') . '</span>'
. mute(glyph('arrow-right'))
. __('Validation')
. mute(glyph('arrow-right'))
. mute(__('Import'))
]
)
. form(
[
div('row', [
div('col-sm-6', [
'<h3>' . __('Rooms to create') . '</h3>',
table(__('Name'), $rooms_new)
]),
div('col-sm-6', [
'<h3>' . __('Rooms to delete') . '</h3>',
table(__('Name'), $rooms_deleted)
])
]),
'<h3>' . __('Shifts to create') . '</h3>',
table([
'day' => __('Day'),
'start' => __('Start'),
'end' => __('End'),
'shifttype' => __('Shift type'),
'title' => __('Title'),
'room' => __('Room')
], shifts_printable($events_new, $shifttypes)),
'<h3>' . __('Shifts to update') . '</h3>',
table([
'day' => __('Day'),
'start' => __('Start'),
'end' => __('End'),
'shifttype' => __('Shift type'),
'title' => __('Title'),
'room' => __('Room')
], shifts_printable($events_updated, $shifttypes)),
'<h3>' . __('Shifts to delete') . '</h3>',
table([
'day' => __('Day'),
'start' => __('Start'),
'end' => __('End'),
'shifttype' => __('Shift type'),
'title' => __('Title'),
'room' => __('Room')
], shifts_printable($events_deleted, $shifttypes)),
form_submit('submit', __('Import'))
],
page_link_to('admin_import', [
'step' => 'import',
'shifttype_id' => $shifttype_id,
'add_minutes_end' => $add_minutes_end,
'add_minutes_start' => $add_minutes_start,
])
);
break;
case 'import':
if (!file_exists($import_file)) {
error(__('Missing import file.'));
throw_redirect(page_link_to('admin_import'));
}
if (!file_exists($import_file)) {
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('shifttype_id') && isset($shifttypes[$request->input('shifttype_id')])) {
$shifttype_id = $request->input('shifttype_id');
} else {
error(__('Please select a shift type.'));
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('add_minutes_start') && is_numeric(trim($request->input('add_minutes_start')))) {
$add_minutes_start = trim($request->input('add_minutes_start'));
} else {
error(__('Please enter an amount of minutes to add to a talk\'s begin.'));
throw_redirect(page_link_to('admin_import'));
}
if ($request->has('add_minutes_end') && is_numeric(trim($request->input('add_minutes_end')))) {
$add_minutes_end = trim($request->input('add_minutes_end'));
} else {
error(__('Please enter an amount of minutes to add to a talk\'s end.'));
throw_redirect(page_link_to('admin_import'));
}
list($rooms_new, $rooms_deleted) = prepare_rooms($import_file);
foreach ($rooms_new as $room) {
$result = Room_create($room, true, null, null);
$rooms_import[trim($room)] = $result;
}
foreach ($rooms_deleted as $room) {
Room_delete_by_name($room);
}
list($events_new, $events_updated, $events_deleted) = prepare_events(
$import_file,
$shifttype_id,
$add_minutes_start,
$add_minutes_end
);
foreach ($events_new as $event) {
Shift_create($event);
}
foreach ($events_updated as $event) {
Shift_update_by_psid($event);
}
foreach ($events_deleted as $event) {
Shift_delete_by_psid($event['PSID']);
}
engelsystem_log('Frab import done');
unlink($import_file);
$html .= div('well well-sm text-center', [
'<span class="text-success">' . __('File Upload') . glyph('ok-circle') . '</span>'
. mute(glyph('arrow-right'))
. '<span class="text-success">' . __('Validation') . glyph('ok-circle') . '</span>'
. mute(glyph('arrow-right'))
. '<span class="text-success">' . __('Import') . glyph('ok-circle') . '</span>'
]) . success(__('It\'s done!'), true);
break;
default:
throw_redirect(page_link_to('admin_import'));
}
return page_with_title(admin_import_title(), [
msg(),
$html
]);
}
/**
* @param string $file
* @return array
*/
function prepare_rooms($file)
{
global $rooms_import;
$data = read_xml($file);
// Load rooms from db for compare with input
$rooms = Rooms();
// Contains rooms from db with from_frab==true
$rooms_db = [];
// Contains all rooms from db
$rooms_db_all = [];
// Contains all rooms from db and frab
$rooms_import = [];
foreach ($rooms as $room) {
if ($room['from_frab']) {
$rooms_db[] = $room['Name'];
}
$rooms_db_all[] = $room['Name'];
$rooms_import[$room['Name']] = $room['RID'];
}
$events = $data->vcalendar->vevent;
$rooms_frab = [];
foreach ($events as $event) {
$rooms_frab[] = (string)$event->location;
if (!isset($rooms_import[trim($event->location)])) {
$rooms_import[trim($event->location)] = trim($event->location);
}
}
$rooms_frab = array_unique($rooms_frab);
$rooms_new = array_diff($rooms_frab, $rooms_db_all);
$rooms_deleted = array_diff($rooms_db, $rooms_frab);
return [
$rooms_new,
$rooms_deleted
];
}
/**
* @param string $file
* @param int $shifttype_id
* @param int $add_minutes_start
* @param int $add_minutes_end
* @return array
*/
function prepare_events($file, $shifttype_id, $add_minutes_start, $add_minutes_end)
{
global $rooms_import;
$data = read_xml($file);
$rooms = Rooms();
$rooms_db = [];
foreach ($rooms as $room) {
$rooms_db[$room['Name']] = $room['RID'];
}
$events = $data->vcalendar->vevent;
$shifts_pb = [];
foreach ($events as $event) {
$event_pb = $event->children('http://pentabarf.org');
$event_id = trim($event_pb->{'event-id'});
$shifts_pb[$event_id] = [
'shifttype_id' => $shifttype_id,
'start' => parse_date("Ymd\THis", $event->dtstart) - $add_minutes_start * 60,
'end' => parse_date("Ymd\THis", $event->dtend) + $add_minutes_end * 60,
'RID' => $rooms_import[trim($event->location)],
'title' => trim($event->summary),
'URL' => trim($event->url),
'PSID' => $event_id
];
}
$shifts = Shifts_from_frab();
$shifts_db = [];
foreach ($shifts as $shift) {
$shifts_db[$shift['PSID']] = $shift;
}
$shifts_new = [];
$shifts_updated = [];
foreach ($shifts_pb as $shift) {
if (!isset($shifts_db[$shift['PSID']])) {
$shifts_new[] = $shift;
} else {
$tmp = $shifts_db[$shift['PSID']];
if (
$shift['shifttype_id'] != $tmp['shifttype_id']
|| $shift['title'] != $tmp['title']
|| $shift['start'] != $tmp['start']
|| $shift['end'] != $tmp['end']
|| $shift['RID'] != $tmp['RID']
|| $shift['URL'] != $tmp['URL']
) {
$shifts_updated[] = $shift;
}
}
}
$shifts_deleted = [];
foreach ($shifts_db as $shift) {
if (!isset($shifts_pb[$shift['PSID']])) {
$shifts_deleted[] = $shift;
}
}
return [
$shifts_new,
$shifts_updated,
$shifts_deleted
];
}
/**
* @param string $file
* @return SimpleXMLElement
*/
function read_xml($file)
{
global $xml_import;
if (!isset($xml_import)) {
libxml_use_internal_errors(true);
$xml_import = simplexml_load_file($file);
}
return $xml_import;
}
/**
* @param array $shifts
* @param array $shifttypes
* @return array
*/
function shifts_printable($shifts, $shifttypes)
{
global $rooms_import;
$rooms = array_flip($rooms_import);
uasort($shifts, 'shift_sort');
$shifts_printable = [];
foreach ($shifts as $shift) {
$shifts_printable[] = [
'day' => date('l, Y-m-d', $shift['start']),
'start' => date('H:i', $shift['start']),
'shifttype' => ShiftType_name_render([
'id' => $shift['shifttype_id'],
'name' => $shifttypes[$shift['shifttype_id']]
]),
'title' => shorten($shift['title']),
'end' => date('H:i', $shift['end']),
'room' => $rooms[$shift['RID']]
];
}
return $shifts_printable;
}
/**
* @param array $shift_a
* @param array $shift_b
* @return int
*/
function shift_sort($shift_a, $shift_b)
{
return ($shift_a['start'] < $shift_b['start']) ? -1 : 1;
}

@ -19,7 +19,6 @@ function admin_rooms()
foreach ($rooms_source as $room) { foreach ($rooms_source as $room) {
$rooms[] = [ $rooms[] = [
'name' => Room_name_render($room), 'name' => Room_name_render($room),
'from_frab' => glyph_bool($room['from_frab']),
'map_url' => glyph_bool(!empty($room['map_url'])), 'map_url' => glyph_bool(!empty($room['map_url'])),
'actions' => table_buttons([ 'actions' => table_buttons([
button( button(
@ -40,7 +39,6 @@ function admin_rooms()
if ($request->has('show')) { if ($request->has('show')) {
$msg = ''; $msg = '';
$name = ''; $name = '';
$from_frab = false;
$map_url = null; $map_url = null;
$description = null; $description = null;
$room_id = 0; $room_id = 0;
@ -61,7 +59,6 @@ function admin_rooms()
$room_id = $request->input('id'); $room_id = $request->input('id');
$name = $room['Name']; $name = $room['Name'];
$from_frab = $room['from_frab'];
$map_url = $room['map_url']; $map_url = $room['map_url'];
$description = $room['description']; $description = $room['description'];
@ -88,8 +85,6 @@ function admin_rooms()
$msg .= error(__('Please enter a name.'), true); $msg .= error(__('Please enter a name.'), true);
} }
$from_frab = $request->has('from_frab');
if ($request->has('map_url')) { if ($request->has('map_url')) {
$map_url = strip_request_item('map_url'); $map_url = strip_request_item('map_url');
} }
@ -118,9 +113,9 @@ function admin_rooms()
if ($valid) { if ($valid) {
if (empty($room_id)) { if (empty($room_id)) {
$room_id = Room_create($name, $from_frab, $map_url, $description); $room_id = Room_create($name, $map_url, $description);
} else { } else {
Room_update($room_id, $name, $from_frab, $map_url, $description); Room_update($room_id, $name, $map_url, $description);
} }
NeededAngelTypes_delete_by_room($room_id); NeededAngelTypes_delete_by_room($room_id);
@ -159,7 +154,6 @@ function admin_rooms()
div('row', [ div('row', [
div('col-md-6', [ div('col-md-6', [
form_text('name', __('Name'), $name, false, 35), form_text('name', __('Name'), $name, false, 35),
form_checkbox('from_frab', __('Frab import'), $from_frab),
form_text('map_url', __('Map URL'), $map_url), form_text('map_url', __('Map URL'), $map_url),
form_info('', __('The map url is used to display an iframe on the room page.')), form_info('', __('The map url is used to display an iframe on the room page.')),
form_textarea('description', __('Description'), $description), form_textarea('description', __('Description'), $description),
@ -212,7 +206,6 @@ function admin_rooms()
msg(), msg(),
table([ table([
'name' => __('Name'), 'name' => __('Name'),
'from_frab' => __('Frab import'),
'map_url' => __('Map'), 'map_url' => __('Map'),
'actions' => '' 'actions' => ''
], $rooms) ], $rooms)

@ -350,7 +350,6 @@ function admin_shifts()
foreach ($session->get('admin_shifts_shifts', []) as $shift) { foreach ($session->get('admin_shifts_shifts', []) as $shift) {
$shift['URL'] = null; $shift['URL'] = null;
$shift['PSID'] = null;
$shift_id = Shift_create($shift); $shift_id = Shift_create($shift);
engelsystem_log( engelsystem_log(

@ -0,0 +1,612 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Controllers\Admin\Schedule;
use Carbon\Carbon;
use Engelsystem\Controllers\BaseController;
use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\Schedule;
use Engelsystem\Helpers\Schedule\XmlParser;
use Engelsystem\Http\Request;
use Engelsystem\Http\Response;
use Engelsystem\Models\Shifts\Schedule as ScheduleUrl;
use Engelsystem\Models\Shifts\ScheduleShift;
use ErrorException;
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;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class ImportSchedule extends BaseController
{
/** @var DatabaseConnection */
protected $db;
/** @var LoggerInterface */
protected $log;
/** @var array */
protected $permissions = [
'schedule.import',
];
/** @var XmlParser */
protected $parser;
/** @var Response */
protected $response;
/** @var SessionInterface */
protected $session;
/** @var string */
protected $url = '/admin/schedule';
/** @var GuzzleClient */
protected $guzzle;
/**
* @param Response $response
* @param SessionInterface $session
* @param GuzzleClient $guzzle
* @param XmlParser $parser
* @param DatabaseConnection $db
* @param LoggerInterface $log
*/
public function __construct(
Response $response,
SessionInterface $session,
GuzzleClient $guzzle,
XmlParser $parser,
DatabaseConnection $db,
LoggerInterface $log
) {
$this->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);
}
}

@ -108,30 +108,38 @@ function make_navigation()
$admin_menu = []; $admin_menu = [];
$admin_pages = [ $admin_pages = [
'admin_arrive' => __('Arrived angels'), 'admin_arrive' => 'Arrived angels',
'admin_active' => __('Active angels'), 'admin_active' => 'Active angels',
'admin_user' => __('All Angels'), 'admin_user' => 'All Angels',
'admin_free' => __('Free angels'), 'admin_free' => 'Free angels',
'admin_questions' => __('Answer questions'), 'admin_questions' => 'Answer questions',
'shifttypes' => __('Shifttypes'), 'shifttypes' => 'Shifttypes',
'admin_shifts' => __('Create shifts'), 'admin_shifts' => 'Create shifts',
'admin_rooms' => __('Rooms'), 'admin_rooms' => 'Rooms',
'admin_groups' => __('Grouprights'), 'admin_groups' => 'Grouprights',
'admin_import' => __('Frab import'), 'admin/schedule' => ['schedule.import', 'schedule.import'],
'admin_log' => __('Log'), 'admin_log' => 'Log',
'admin_event_config' => __('Event config'), 'admin_event_config' => 'Event config',
]; ];
if (config('autoarrive')) { if (config('autoarrive')) {
unset($admin_pages['admin_arrive']); unset($admin_pages['admin_arrive']);
} }
foreach ($admin_pages as $menu_page => $title) { foreach ($admin_pages as $menu_page => $options) {
if (auth()->can($menu_page)) { $options = (array)$options;
$permissions = $menu_page;
$title = $options[0];
if (isset($options[1])) {
$permissions = $options[1];
}
if (auth()->can($permissions)) {
$admin_menu[] = toolbar_item_link( $admin_menu[] = toolbar_item_link(
page_link_to($menu_page), page_link_to($menu_page),
'', '',
$title, __($title),
$menu_page == $page $menu_page == $page
); );
} }

@ -409,42 +409,6 @@ function table_buttons($buttons = [])
return '<div class="btn-group">' . join(' ', $buttons) . '</div>'; return '<div class="btn-group">' . join(' ', $buttons) . '</div>';
} }
/**
* @param string $str
* @param int $length
* @return string
*/
function shorten($str, $length = 50)
{
if (strlen($str) < $length) {
return $str;
}
return '<span title="' . htmlentities($str, ENT_COMPAT, 'UTF-8') . '">'
. substr($str, 0, $length - 3)
. '...</span>';
}
/**
* @param array[] $array
* @return string
*/
function table_body($array)
{
$html = '';
foreach ($array as $line) {
$html .= '<tr>';
if (is_array($line)) {
foreach ($line as $td) {
$html .= '<td>' . $td . '</td>';
}
} else {
$html .= '<td>' . $line . '</td>';
}
$html .= '</tr>';
}
return $html;
}
/** /**
* @param string $msg * @param string $msg
* @return mixed * @return mixed

@ -34,3 +34,36 @@ msgstr "Deine Passwörter stimmen nicht überein."
msgid "validation.password_confirmation.required" msgid "validation.password_confirmation.required"
msgstr "Du musst dein Passwort bestätigen." msgstr "Du musst dein Passwort bestätigen."
msgid "schedule.import"
msgstr "Programm importieren"
msgid "schedule.import.request-error"
msgstr "Das Programm konnte nicht abgerufen werden."
msgid "schedule.import.read-error"
msgstr "Das Programm konnte nicht gelesen werden."
msgid "schedule.import.invalid-shift-type"
msgstr "Der Schichttyp konnte nicht gefunden werden."
msgid "schedule.import.success"
msgstr "Das Programm wurde erfolgreich importiert."
msgid "validation.schedule-url.required"
msgstr "Bitte gib eine Programm URL an."
msgid "validation.schedule-url.url"
msgstr "Die Programm URL muss eine URL sein."
msgid "validation.shift-type.required"
msgstr "Der Schichttyp ist erforderlich."
msgid "validation.shift-type.int"
msgstr "Der Schichttyp muss eine Zahl sein."
msgid "validation.minutes-before.int"
msgstr "Die Minuten vor dem Talk müssen eine Zahl sein."
msgid "validation.minutes-after.int"
msgstr "Die Minuten nach dem Talk müssen eine Zahl sein."

@ -2806,3 +2806,60 @@ msgstr ""
#~ msgid "auth.no-nickname" #~ msgid "auth.no-nickname"
#~ msgstr "Gib bitte einen Nick an." #~ msgstr "Gib bitte einen Nick an."
msgid "form.load_schedule"
msgstr "Programm laden"
msgid "form.import"
msgstr "Importieren"
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."
msgid "schedule.import.load.title"
msgstr "Programm importieren: Vorschau"
msgid "schedule.import.load.info"
msgstr "Importiere \"%s\" (Version \"%s\")"
msgid "schedule.url"
msgstr "Programm URL (schedule.xml)"
msgid "schedule.shift-type"
msgstr "Schichttyp"
msgid "schedule.minutes-before"
msgstr "Minuten vor Talk beginn hinzufügen"
msgid "schedule.minutes-after"
msgstr "Minuten nach Talk ende hinzufügen"
msgid "schedule.import.rooms.add"
msgstr "Neue Räume"
msgid "schedule.import.shifts.add"
msgstr "Neue Schichten"
msgid "schedule.import.shifts.update"
msgstr "Zu aktualisierende Schichten"
msgid "schedule.import.shifts.delete"
msgstr "Zu löschende Schichten"
msgid "schedule.import.rooms.name"
msgstr "Name"
msgid "schedule.import.shift.dates"
msgstr "Zeit"
msgid "schedule.import.shift.type"
msgstr "Typ"
msgid "schedule.import.shift.title"
msgstr "Titel"
msgid "schedule.import.shift.room"
msgstr "Raum"

@ -32,3 +32,36 @@ msgstr "Your passwords are not equal."
msgid "validation.password_confirmation.required" msgid "validation.password_confirmation.required"
msgstr "You have to confirm your password." msgstr "You have to confirm your password."
msgid "schedule.import"
msgstr "Import schedule"
msgid "schedule.import.request-error"
msgstr "The schedule could not be requested."
msgid "schedule.import.read-error"
msgstr "Unable to parse schedule."
msgid "schedule.import.invalid-shift-type"
msgstr "The shift type can't not be found."
msgid "schedule.import.success"
msgstr "Schedule import successful."
msgid "validation.schedule-url.required"
msgstr "The schedule URL is required."
msgid "validation.schedule-url.url"
msgstr "The schedule URL needs to be of type URL."
msgid "validation.shift-type.required"
msgstr "The shift type is required."
msgid "validation.shift-type.int"
msgstr "The shift type has to ba a number."
msgid "validation.minutes-before.int"
msgstr "The minutes before the talk have to be an integer."
msgid "validation.minutes-after.int"
msgstr "The minutes after the talk have to be an integer."

@ -45,3 +45,63 @@ msgstr ""
"Please have a look at the " "Please have a look at the "
"[contributors list on GitHub](https://github.com/engelsystem/engelsystem/graphs/contributors)" "[contributors list on GitHub](https://github.com/engelsystem/engelsystem/graphs/contributors)"
" for a complete list." " for a complete list."
msgid "form.load_schedule"
msgstr "Load schedule"
msgid "form.import"
msgstr "Import"
msgid "schedule.import.title"
msgstr "Import schedule"
msgid "schedule.import.text"
msgstr "This import creates rooms and creates, updates and deletes shifts according to the schedule.xml export."
msgid "schedule.import.load.title"
msgstr "Import schedule: Preview"
msgid "schedule.import.load.info"
msgstr "Import \"%s\" (version \"%s\")"
msgid "schedule.url"
msgstr "Schedule URL (schedule.xml)"
msgid "schedule.shift-type"
msgstr "Shift type"
msgid "schedule.minutes-before"
msgstr "Add minutes before talk begins"
msgid "schedule.minutes-after"
msgstr "Add minutes after talk ends"
msgid "schedule.import.request_error"
msgstr "Unable to load schedule."
msgid "schedule.import.rooms.add"
msgstr "Rooms to create"
msgid "schedule.import.shifts.add"
msgstr "Shifts to create"
msgid "schedule.import.shifts.update"
msgstr "Shifts to update"
msgid "schedule.import.shifts.delete"
msgstr "Shifts to delete"
msgid "schedule.import.rooms.name"
msgstr "Name"
msgid "schedule.import.shift.dates"
msgstr "Times"
msgid "schedule.import.shift.type"
msgstr "Type"
msgid "schedule.import.shift.title"
msgstr "Title"
msgid "schedule.import.shift.room"
msgstr "Room"

@ -0,0 +1,41 @@
{% extends 'layouts/app.twig' %}
{% import 'macros/base.twig' as m %}
{% import 'macros/form.twig' as f %}
{% set title %}{% block title %}{{ __('schedule.import.title') }}{% endblock %}{% endset %}
{% block content %}
<div class="container">
<h1>{% block content_title %}{{ title }}{% endblock %}</h1>
{% for message in errors|default([]) %}
{{ m.alert(__(message), 'danger') }}
{% endfor %}
{% for message in success|default([]) %}
{{ m.alert(__(message), 'success') }}
{% endfor %}
<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>
<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>
{% endblock %}

@ -0,0 +1,79 @@
{% extends 'admin/schedule/index.twig' %}
{% import 'macros/form.twig' as f %}
{% block title %}{{ __('schedule.import.load.title') }}{% endblock %}
{% block row_content %}
<form method="POST" action="{{ url('/admin/schedule/import') }}">
{{ 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>
<h2>{{ __('schedule.import.rooms.add') }}</h2>
{{ _self.roomsTable(rooms.add) }}
<h2>{{ __('schedule.import.shifts.add') }}</h2>
{{ _self.shiftsTable(shifts.add) }}
<h2>{{ __('schedule.import.shifts.update') }}</h2>
{{ _self.shiftsTable(shifts.update) }}
<h2>{{ __('schedule.import.shifts.delete') }}</h2>
{{ _self.shiftsTable(shifts.delete) }}
{{ f.submit(__('form.import')) }}
</div>
</form>
{% endblock %}
{% macro roomsTable(rooms) %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{{ __('schedule.import.rooms.name') }}</th>
</tr>
</thead>
<tbody>
{% for room in rooms %}
<tr>
<td>{{ room.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}
{% macro shiftsTable(shifts) %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>{{ __('schedule.import.shift.dates') }}</th>
<th>{{ __('schedule.import.shift.type') }}</th>
<th>{{ __('schedule.import.shift.title') }}</th>
<th>{{ __('schedule.import.shift.room') }}</th>
</tr>
</thead>
<tbody>
{% for shift in shifts %}
<tr>
<td>{{ shift.date.format(__('Y-m-d H:i')) }} - {{ shift.endDate.format(__('H:i')) }}</td>
<td>{{ shift.type }}</td>
<td>{{ shift.title }}{% if shift.subtitle %}<br><small>{{ shift.subtitle }}</small>{% endif %}</td>
<td>{{ shift.room.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endmacro %}

@ -13,6 +13,19 @@
</div> </div>
{%- endmacro %} {%- endmacro %}
{% macro select(name, data, label, selected) %}
<div class="form-group">
{% if label -%}
<label for="{{ name }}">{{ label }}</label>
{% endif %}
<select id="{{ name }}" name="{{ name }}" class="form-control">
{% for value,decription in data -%}
<option value="{{ value }}" {% if name == selected %} selected{% endif %}>{{ decription }}</option>
{% endfor %}
</select>
</div>
{%- endmacro %}
{% macro hidden(name, value) %} {% macro hidden(name, value) %}
<input type="hidden" id="{{ name }}" name="{{ name }}" value="{{ value }}"> <input type="hidden" id="{{ name }}" name="{{ name }}" value="{{ value }}">
{%- endmacro %} {%- endmacro %}

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
trait CalculatesTime
{
/**
* @param string $time
* @return int
*/
protected function secondsFromTime(string $time): int
{
$seconds = 0;
$duration = explode(':', $time);
foreach (array_slice($duration, 0, 2) as $key => $times) {
$seconds += [60 * 60, 60][$key] * $times;
}
return $seconds;
}
}

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
class Conference
{
use CalculatesTime;
/** @var string required */
protected $title;
/** @var string required */
protected $acronym;
/** @var string|null */
protected $start;
/** @var string|null */
protected $end;
/** @var int|null */
protected $days;
/** @var string|null */
protected $timeslotDuration;
/** @var string|null */
protected $baseUrl;
/**
* Event constructor.
*
* @param string $title
* @param string $acronym
* @param string|null $start
* @param string|null $end
* @param int|null $days
* @param string|null $timeslotDuration
* @param string|null $baseUrl
*/
public function __construct(
string $title,
string $acronym,
?string $start = null,
?string $end = null,
?int $days = null,
?string $timeslotDuration = null,
?string $baseUrl = null
) {
$this->title = $title;
$this->acronym = $acronym;
$this->start = $start;
$this->end = $end;
$this->days = $days;
$this->timeslotDuration = $timeslotDuration;
$this->baseUrl = $baseUrl;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string
*/
public function getAcronym(): string
{
return $this->acronym;
}
/**
* @return string|null
*/
public function getStart(): ?string
{
return $this->start;
}
/**
* @return string|null
*/
public function getEnd(): ?string
{
return $this->end;
}
/**
* @return int|null
*/
public function getDays(): ?int
{
return $this->days;
}
/**
* @return string|null
*/
public function getTimeslotDuration(): ?string
{
return $this->timeslotDuration;
}
/**
* @return int|null
*/
public function getTimeslotDurationSeconds(): ?int
{
$duration = $this->getTimeslotDuration();
if (!$duration) {
return null;
}
return $this->secondsFromTime($duration);
}
/**
* @return string|null
*/
public function getBaseUrl(): ?string
{
return $this->baseUrl;
}
}

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
use Carbon\Carbon;
class Day
{
/** @var string required */
protected $date;
/** @var Carbon required */
protected $start;
/** @var Carbon required */
protected $end;
/** @var int required */
protected $index;
/** @var Room[] */
protected $room;
/**
* Day constructor.
*
* @param string $date
* @param Carbon $start
* @param Carbon $end
* @param int $index
* @param Room[] $rooms
*/
public function __construct(
string $date,
Carbon $start,
Carbon $end,
int $index,
array $rooms = []
) {
$this->date = $date;
$this->start = $start;
$this->end = $end;
$this->index = $index;
$this->room = $rooms;
}
/**
* @return string
*/
public function getDate(): string
{
return $this->date;
}
/**
* @return Carbon
*/
public function getStart(): Carbon
{
return $this->start;
}
/**
* @return Carbon
*/
public function getEnd(): Carbon
{
return $this->end;
}
/**
* @return int
*/
public function getIndex(): int
{
return $this->index;
}
/**
* @return Room[]
*/
public function getRoom(): array
{
return $this->room;
}
}

@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
use Carbon\Carbon;
class Event
{
use CalculatesTime;
/** @var string required globally unique */
protected $guid;
/** @var int required globally unique */
protected $id;
/** @var Room required, string in XML */
protected $room;
/** @var string required */
protected $title;
/** @var string required */
protected $subtitle;
/** @var string required */
protected $type;
/** @var Carbon required */
protected $date;
/** @var string required time (hh:mm:ss || hh:mm) */
protected $start;
/** @var string required (h?h:mm:ss || h?h:mm) */
protected $duration;
/** @var string required */
protected $abstract;
/** @var string required globally unique */
protected $slug;
/** @var string required */
protected $track;
/** @var string|null */
protected $logo;
/** @var string[] id => name */
protected $persons;
/** @var string|null two letter code */
protected $language;
/** @var string|null */
protected $description;
/** @var string|null license (and opt out in XML, null if not recorded, empty if no license defined) */
protected $recording;
/** @var array href => title */
protected $links;
/** @var array href => name */
protected $attachments;
/** @var string|null */
protected $url;
/** @var string|null */
protected $videoDownloadUrl;
/** @var Carbon Calculated */
protected $endDate;
/**
* Event constructor.
*
* @param string $guid
* @param int $id
* @param Room $room
* @param string $title
* @param string $subtitle
* @param string $type
* @param Carbon $date
* @param string $start
* @param string $duration
* @param string $abstract
* @param string $slug
* @param string $track
* @param string|null $logo
* @param string[] $persons
* @param string|null $language
* @param string|null $description
* @param string|null $recording license
* @param array $links
* @param array $attachments
* @param string|null $url
* @param string|null $videoDownloadUrl
*/
public function __construct(
string $guid,
int $id,
Room $room,
string $title,
string $subtitle,
string $type,
Carbon $date,
string $start,
string $duration,
string $abstract,
string $slug,
string $track,
?string $logo = null,
array $persons = [],
?string $language = null,
?string $description = null,
string $recording = '',
array $links = [],
array $attachments = [],
?string $url = null,
?string $videoDownloadUrl = null
) {
$this->guid = $guid;
$this->id = $id;
$this->room = $room;
$this->title = $title;
$this->subtitle = $subtitle;
$this->type = $type;
$this->date = $date;
$this->start = $start;
$this->duration = $duration;
$this->abstract = $abstract;
$this->slug = $slug;
$this->track = $track;
$this->logo = $logo;
$this->persons = $persons;
$this->language = $language;
$this->description = $description;
$this->recording = $recording;
$this->links = $links;
$this->attachments = $attachments;
$this->url = $url;
$this->videoDownloadUrl = $videoDownloadUrl;
$this->endDate = $this->date
->copy()
->addSeconds($this->getDurationSeconds());
}
/**
* @return string
*/
public function getGuid(): string
{
return $this->guid;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return Room
*/
public function getRoom(): Room
{
return $this->room;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string
*/
public function getSubtitle(): string
{
return $this->subtitle;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @return Carbon
*/
public function getDate(): Carbon
{
return $this->date;
}
/**
* @return string
*/
public function getStart(): string
{
return $this->start;
}
/**
* @return string
*/
public function getDuration(): string
{
return $this->duration;
}
/**
* @return int
*/
public function getDurationSeconds(): int
{
return $this->secondsFromTime($this->duration);
}
/**
* @return string
*/
public function getAbstract(): string
{
return $this->abstract;
}
/**
* @return string
*/
public function getSlug(): string
{
return $this->slug;
}
/**
* @return string
*/
public function getTrack(): string
{
return $this->track;
}
/**
* @return string|null
*/
public function getLogo(): ?string
{
return $this->logo;
}
/**
* @return string[]
*/
public function getPersons(): array
{
return $this->persons;
}
/**
* @return string|null
*/
public function getLanguage(): ?string
{
return $this->language;
}
/**
* @return string|null
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @return string|null
*/
public function getRecording(): ?string
{
return $this->recording;
}
/**
* @return array
*/
public function getLinks(): array
{
return $this->links;
}
/**
* @return array
*/
public function getAttachments(): array
{
return $this->attachments;
}
/**
* @return string|null
*/
public function getUrl(): ?string
{
return $this->url;
}
/**
* @return string|null
*/
public function getVideoDownloadUrl(): ?string
{
return $this->videoDownloadUrl;
}
/**
* @return Carbon
*/
public function getEndDate(): Carbon
{
return $this->endDate;
}
}

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
class Room
{
/** @var string required */
protected $name;
/** @var Event[] */
protected $event;
/**
* Room constructor.
*
* @param string $name
* @param Event[] $events
*/
public function __construct(
string $name,
array $events = []
) {
$this->name = $name;
$this->event = $events;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return Event[]
*/
public function getEvent(): array
{
return $this->event;
}
/**
* @param Event[] $event
*/
public function setEvent(array $event): void
{
$this->event = $event;
}
}

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
use Carbon\Carbon;
class Schedule
{
/** @var string */
protected $version;
/** @var Conference */
protected $conference;
/** @var Day[] */
protected $day;
/**
* @param string $version
* @param Conference $conference
* @param Day[] $days
*/
public function __construct(
string $version,
Conference $conference,
array $days
) {
$this->version = $version;
$this->conference = $conference;
$this->day = $days;
}
/**
* @return string
*/
public function getVersion(): string
{
return $this->version;
}
/**
* @return Conference
*/
public function getConference(): Conference
{
return $this->conference;
}
/**
* @return Day[]
*/
public function getDay(): array
{
return $this->day;
}
/**
* @return Room[]
*/
public function getRooms(): array
{
$rooms = [];
foreach ($this->day as $day) {
foreach ($day->getRoom() as $room) {
$name = $room->getName();
$rooms[$name] = $room;
}
}
return $rooms;
}
/**
* @return Carbon|null
*/
public function getStartDateTime(): ?Carbon
{
$start = null;
foreach ($this->day as $day) {
$time = $day->getStart();
if ($time > $start && $start) {
continue;
}
$start = $time;
}
return $start;
}
/**
* @return Carbon|null
*/
public function getEndDateTime(): ?Carbon
{
$end = null;
foreach ($this->day as $day) {
$time = $day->getEnd();
if ($time < $end && $end) {
continue;
}
$end = $time;
}
return $end;
}
}

@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Engelsystem\Helpers\Schedule;
use Carbon\Carbon;
use SimpleXMLElement;
class XmlParser
{
/** @var SimpleXMLElement */
protected $scheduleXML;
/** @var Schedule */
protected $schedule;
/**
* @param string $xml
* @return bool
*/
public function load(string $xml): bool
{
$this->scheduleXML = simplexml_load_string($xml);
if (!$this->scheduleXML) {
return false;
}
$this->parseXml();
return true;
}
/**
* Parse the predefined XML content
*/
protected function parseXml(): void
{
$version = $this->getFirstXpathContent('version');
$conference = new Conference(
$this->getFirstXpathContent('conference/title'),
$this->getFirstXpathContent('conference/acronym'),
$this->getFirstXpathContent('conference/start'),
$this->getFirstXpathContent('conference/end'),
(int)$this->getFirstXpathContent('conference/days'),
$this->getFirstXpathContent('conference/timeslot_duration'),
$this->getFirstXpathContent('conference/base_url')
);
$days = [];
foreach ($this->scheduleXML->xpath('day') as $day) {
$rooms = [];
foreach ($day->xpath('room') as $roomElement) {
$room = new Room(
(string)$roomElement->attributes()['name']
);
$events = $this->parseEvents($roomElement->xpath('event'), $room);
$room->setEvent($events);
$rooms[] = $room;
}
$days[] = new Day(
(string)$day->attributes()['date'],
new Carbon($day->attributes()['start']),
new Carbon($day->attributes()['end']),
(int)$day->attributes()['index'],
$rooms
);
}
$this->schedule = new Schedule(
$version,
$conference,
$days
);
}
/**
* @param SimpleXMLElement[] $eventElements
* @param Room $room
* @return array
*/
protected function parseEvents(array $eventElements, Room $room): array
{
$events = [];
foreach ($eventElements as $event) {
$persons = $this->getListFromSequence($event, 'persons', 'person', 'id');
$links = $this->getListFromSequence($event, 'links', 'link', 'href');
$attachments = $this->getListFromSequence($event, 'attachments', 'attachment', 'href');
$recording = '';
$recordingElement = $event->xpath('recording')[0];
if ($this->getFirstXpathContent('optout', $recordingElement) == 'false') {
$recording = $this->getFirstXpathContent('license', $recordingElement);
}
$events[] = new Event(
(string)$event->attributes()['guid'],
(int)$event->attributes()['id'],
$room,
$this->getFirstXpathContent('title', $event),
$this->getFirstXpathContent('subtitle', $event),
$this->getFirstXpathContent('type', $event),
new Carbon($this->getFirstXpathContent('date', $event)),
$this->getFirstXpathContent('start', $event),
$this->getFirstXpathContent('duration', $event),
$this->getFirstXpathContent('abstract', $event),
$this->getFirstXpathContent('slug', $event),
$this->getFirstXpathContent('track', $event),
$this->getFirstXpathContent('logo', $event) ?: null,
$persons,
$this->getFirstXpathContent('language', $event) ?: null,
$this->getFirstXpathContent('description', $event) ?: null,
$recording,
$links,
$attachments,
$this->getFirstXpathContent('url', $event) ?: null,
$this->getFirstXpathContent('video_download_url', $event) ?: null
);
}
return $events;
}
/**
* @param string $path
* @param SimpleXMLElement|null $xml
* @return string
*/
protected function getFirstXpathContent(string $path, ?SimpleXMLElement $xml = null): string
{
$element = ($xml ?: $this->scheduleXML)->xpath($path);
return $element ? (string)$element[0] : '';
}
/**
* Resolves a list from a sequence of elements
*
* @param SimpleXMLElement $element
* @param string $firstElement
* @param string $secondElement
* @param string $idAttribute
* @return array
*/
protected function getListFromSequence(
SimpleXMLElement $element,
string $firstElement,
string $secondElement,
string $idAttribute
): array {
$items = [];
foreach ($element->xpath($firstElement)[0]->xpath($secondElement) as $item) {
$items[(string)$item->attributes()[$idAttribute]] = (string)$item;
}
return $items;
}
/**
* @return Schedule
*/
public function getSchedule(): Schedule
{
return $this->schedule;
}
}

@ -0,0 +1,25 @@
<?php
namespace Engelsystem\Http;
use Engelsystem\Container\ServiceProvider;
use GuzzleHttp\Client as GuzzleClient;
class GuzzleServiceProvider extends ServiceProvider
{
public function register()
{
$this->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,
];
}
);
}
}

@ -205,10 +205,6 @@ class LegacyMiddleware implements MiddlewareInterface
$title = admin_groups_title(); $title = admin_groups_title();
$content = admin_groups(); $content = admin_groups();
return [$title, $content]; return [$title, $content];
case 'admin_import':
$title = admin_import_title();
$content = admin_import();
return [$title, $content];
case 'admin_shifts': case 'admin_shifts':
$title = admin_shifts_title(); $title = admin_shifts_title();
$content = admin_shifts(); $content = admin_shifts();
@ -239,9 +235,15 @@ class LegacyMiddleware implements MiddlewareInterface
return response($content, (int)$page); return response($content, (int)$page);
} }
return response(view('layouts/app', [ return response(
view(
'layouts/app',
[
'title' => $title, 'title' => $title,
'content' => msg() . $content, 'content' => msg() . $content,
]), 200); ]
),
200
);
} }
} }

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Models\Shifts;
use Engelsystem\Models\BaseModel;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Query\Builder as QueryBuilder;
/**
* @property int $id
* @property string $url
*
* @property-read QueryBuilder|Collection|ScheduleShift[] $scheduleShifts
*
* @method static QueryBuilder|Schedule[] whereId($value)
* @method static QueryBuilder|Schedule[] whereUrl($value)
*/
class Schedule extends BaseModel
{
/** @var array Values that are mass assignable */
protected $fillable = ['url'];
/**
* @return HasMany
*/
public function scheduleShifts()
{
return $this->hasMany(ScheduleShift::class);
}
}

@ -0,0 +1,38 @@
<?php
namespace Engelsystem\Models\Shifts;
use Engelsystem\Models\BaseModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Query\Builder as QueryBuilder;
/**
* @property int $shift_id
* @property int $schedule_id
* @property string $guid
*
* @property-read QueryBuilder|Schedule $schedule
*
* @method static QueryBuilder|ScheduleShift[] whereShiftId($value)
* @method static QueryBuilder|ScheduleShift[] whereScheduleId($value)
* @method static QueryBuilder|ScheduleShift[] whereGuid($value)
*/
class ScheduleShift extends BaseModel
{
/** @var string The primary key for the model */
protected $primaryKey = 'shift_id';
/** @var string Required because it is not schedule_shifts */
protected $table = 'schedule_shift';
/** @var array Values that are mass assignable */
protected $fillable = ['shift_id', 'schedule_id', 'guid'];
/**
* @return BelongsTo
*/
public function schedule()
{
return $this->belongsTo(Schedule::class);
}
}

@ -13,7 +13,7 @@ class RoomModelTest extends TestCase
*/ */
public function createRoom() public function createRoom()
{ {
$this->room_id = Room_create('test', false, null, null); $this->room_id = Room_create('test', null, null);
} }
/** /**

@ -36,14 +36,17 @@ trait HasDatabase
$this->database $this->database
->getConnection() ->getConnection()
->table('migrations') ->table('migrations')
->insert([ ->insert(
[
['migration' => '2018_01_01_000001_import_install_sql'], ['migration' => '2018_01_01_000001_import_install_sql'],
['migration' => '2018_01_01_000002_import_update_sql'], ['migration' => '2018_01_01_000002_import_update_sql'],
['migration' => '2018_01_01_000003_fix_old_tables'], ['migration' => '2018_01_01_000003_fix_old_tables'],
['migration' => '2018_01_01_000004_cleanup_group_privileges'], ['migration' => '2018_01_01_000004_cleanup_group_privileges'],
['migration' => '2018_01_01_000005_add_angel_supporter_permissions'], ['migration' => '2018_01_01_000005_add_angel_supporter_permissions'],
['migration' => '2018_12_27_000000_fix_missing_arrival_dates'], ['migration' => '2018_12_27_000000_fix_missing_arrival_dates'],
]); ['migration' => '2019_09_07_000000_migrate_admin_schedule_permissions'],
]
);
$migration->run(__DIR__ . '/../../db/migrations'); $migration->run(__DIR__ . '/../../db/migrations');
} }

@ -0,0 +1,46 @@
<?xml version='1.0' encoding='utf-8' ?>
<schedule>
<version>Some version string</version>
<conference>
<title>Test Event</title>
<acronym>Test1</acronym>
<start>2042-01-01</start>
<end>2042-01-01</end>
<days>1</days>
<timeslot_duration>00:15</timeslot_duration>
<base_url>https://foo.bar/baz/schedule/</base_url>
</conference>
<day index='1' date='2042-01-01' start='2042-01-01T01:00:00+02:00' end='2042-01-01T22:59:00+02:00'>
<room name='Rooming'>
<event guid='e427cfa9-9ba1-4b14-a99f-bce83ffe5a1c' id='1337'>
<date>2042-01-01T12:30:00+02:00</date>
<title>Foo Bar Test</title>
<subtitle>Some sub</subtitle>
<start>12:30</start>
<duration>00:30</duration>
<room>Rooming</room>
<slug>foo-bar-test</slug>
<recording>
<license>WTFPL</license>
<optout>false</optout>
</recording>
<track>Testing</track>
<type>Talk</type>
<language>de</language>
<abstract>Foo bar is da best</abstract>
<description>Any describing stuff?</description>
<url>https://foo.bar/baz/schedule/ipsum</url>
<logo>https://lorem.ipsum/foo/bar.png</logo>
<persons>
<person id='1234'>Some Person</person>
</persons>
<links>
<link href="https://foo.bar">Some Foo Bar</link>
</links>
<attachments>
<attachment href="https://foo.bar/stuff.pdf">A PDF File</attachment>
</attachments>
</event>
</room>
</day>
</schedule>

@ -0,0 +1,33 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Engelsystem\Helpers\Schedule\CalculatesTime;
use Engelsystem\Test\Unit\TestCase;
class CalculatesTimeTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\CalculatesTime::secondsFromTime
*/
public function testSecondsFromTime()
{
$calc = new class {
use CalculatesTime;
/**
* @param string $time
* @return int
*/
public function calc(string $time): int
{
return $this->secondsFromTime($time);
}
};
$this->assertEquals(0, $calc->calc('0:00'));
$this->assertEquals(60, $calc->calc('0:01'));
$this->assertEquals(60 * 60, $calc->calc('01:00'));
$this->assertEquals(60 * 60 * 10 + 60 * 11, $calc->calc('10:11'));
}
}

@ -0,0 +1,49 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Engelsystem\Helpers\Schedule\Conference;
use Engelsystem\Test\Unit\TestCase;
class ConferenceTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\Conference::__construct
* @covers \Engelsystem\Helpers\Schedule\Conference::getTitle
* @covers \Engelsystem\Helpers\Schedule\Conference::getAcronym
* @covers \Engelsystem\Helpers\Schedule\Conference::getStart
* @covers \Engelsystem\Helpers\Schedule\Conference::getEnd
* @covers \Engelsystem\Helpers\Schedule\Conference::getDays
* @covers \Engelsystem\Helpers\Schedule\Conference::getTimeslotDuration
* @covers \Engelsystem\Helpers\Schedule\Conference::getTimeslotDurationSeconds
* @covers \Engelsystem\Helpers\Schedule\Conference::getBaseUrl
*/
public function testCreate()
{
$conference = new Conference('Doing stuff', 'DS');
$this->assertEquals('Doing stuff', $conference->getTitle());
$this->assertEquals('DS', $conference->getAcronym());
$this->assertNull($conference->getStart());
$this->assertNull($conference->getEnd());
$this->assertNull($conference->getDays());
$this->assertNull($conference->getTimeslotDuration());
$this->assertNull($conference->getTimeslotDurationSeconds());
$this->assertNull($conference->getBaseUrl());
$conference = new Conference(
'Doing stuff',
'DS',
'2042-01-01',
'2042-01-10',
10,
'00:10',
'https://foo.bar/schedule'
);
$this->assertEquals('2042-01-01', $conference->getStart());
$this->assertEquals('2042-01-10', $conference->getEnd());
$this->assertEquals(10, $conference->getDays());
$this->assertEquals('00:10', $conference->getTimeslotDuration());
$this->assertEquals(60 * 10, $conference->getTimeslotDurationSeconds());
$this->assertEquals('https://foo.bar/schedule', $conference->getBaseUrl());
}
}

@ -0,0 +1,47 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Carbon\Carbon;
use Engelsystem\Helpers\Schedule\Day;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Test\Unit\TestCase;
class DayTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\Day::__construct
* @covers \Engelsystem\Helpers\Schedule\Day::getDate
* @covers \Engelsystem\Helpers\Schedule\Day::getStart
* @covers \Engelsystem\Helpers\Schedule\Day::getEnd
* @covers \Engelsystem\Helpers\Schedule\Day::getIndex
* @covers \Engelsystem\Helpers\Schedule\Day::getRoom
*/
public function testCreate()
{
$day = new Day(
'2000-01-01',
new Carbon('2000-01-01T03:00:00+01:00'),
new Carbon('2000-01-02T05:59:00+00:00'),
1
);
$this->assertEquals('2000-01-01', $day->getDate());
$this->assertEquals('2000-01-01T03:00:00+01:00', $day->getStart()->format(Carbon::RFC3339));
$this->assertEquals('2000-01-02T05:59:00+00:00', $day->getEnd()->format(Carbon::RFC3339));
$this->assertEquals(1, $day->getIndex());
$this->assertEquals([], $day->getRoom());
$rooms = [
new Room('Foo'),
new Room('Bar'),
];
$day = new Day(
'2001-01-01',
new Carbon('2001-01-01T03:00:00+01:00'),
new Carbon('2001-01-02T05:59:00+00:00'),
1,
$rooms
);
$this->assertEquals($rooms, $day->getRoom());
}
}

@ -0,0 +1,145 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Carbon\Carbon;
use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Test\Unit\TestCase;
class EventTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\Event::__construct
* @covers \Engelsystem\Helpers\Schedule\Event::getGuid
* @covers \Engelsystem\Helpers\Schedule\Event::getId
* @covers \Engelsystem\Helpers\Schedule\Event::getRoom
* @covers \Engelsystem\Helpers\Schedule\Event::getTitle
* @covers \Engelsystem\Helpers\Schedule\Event::getSubtitle
* @covers \Engelsystem\Helpers\Schedule\Event::getType
* @covers \Engelsystem\Helpers\Schedule\Event::getDate
* @covers \Engelsystem\Helpers\Schedule\Event::getStart
* @covers \Engelsystem\Helpers\Schedule\Event::getDuration
* @covers \Engelsystem\Helpers\Schedule\Event::getDurationSeconds
* @covers \Engelsystem\Helpers\Schedule\Event::getAbstract
* @covers \Engelsystem\Helpers\Schedule\Event::getSlug
* @covers \Engelsystem\Helpers\Schedule\Event::getTrack
* @covers \Engelsystem\Helpers\Schedule\Event::getLogo
* @covers \Engelsystem\Helpers\Schedule\Event::getPersons
* @covers \Engelsystem\Helpers\Schedule\Event::getLanguage
* @covers \Engelsystem\Helpers\Schedule\Event::getDescription
* @covers \Engelsystem\Helpers\Schedule\Event::getRecording
* @covers \Engelsystem\Helpers\Schedule\Event::getLinks
* @covers \Engelsystem\Helpers\Schedule\Event::getAttachments
* @covers \Engelsystem\Helpers\Schedule\Event::getUrl
* @covers \Engelsystem\Helpers\Schedule\Event::getVideoDownloadUrl
* @covers \Engelsystem\Helpers\Schedule\Event::getEndDate
*/
public function testCreate()
{
$room = new Room('Foo');
$date = new Carbon('2020-12-28T19:30:00+00:00');
$event = new Event(
'0-1-2-3',
1,
$room,
'Some stuff',
'sub stuff',
'Talk',
$date,
'19:30:00',
'00:50',
'Doing stuff is hard, plz try again',
'1-some-stuff',
'Security'
);
$this->assertEquals('0-1-2-3', $event->getGuid());
$this->assertEquals(1, $event->getId());
$this->assertEquals($room, $event->getRoom());
$this->assertEquals('Some stuff', $event->getTitle());
$this->assertEquals('sub stuff', $event->getSubtitle());
$this->assertEquals('Talk', $event->getType());
$this->assertEquals($date, $event->getDate());
$this->assertEquals('19:30:00', $event->getStart());
$this->assertEquals('00:50', $event->getDuration());
$this->assertEquals('Doing stuff is hard, plz try again', $event->getAbstract());
$this->assertEquals('1-some-stuff', $event->getSlug());
$this->assertEquals('Security', $event->getTrack());
$this->assertNull($event->getLogo());
$this->assertEquals([], $event->getPersons());
$this->assertNull($event->getLanguage());
$this->assertNull($event->getDescription());
$this->assertEquals('', $event->getRecording());
$this->assertEquals([], $event->getLinks());
$this->assertEquals([], $event->getAttachments());
$this->assertNull($event->getUrl());
$this->assertNull($event->getVideoDownloadUrl());
$this->assertEquals('2020-12-28T20:20:00+00:00', $event->getEndDate()->format(Carbon::RFC3339));
}
/**
* @covers \Engelsystem\Helpers\Schedule\Event::__construct
* @covers \Engelsystem\Helpers\Schedule\Event::getGuid
* @covers \Engelsystem\Helpers\Schedule\Event::getId
* @covers \Engelsystem\Helpers\Schedule\Event::getRoom
* @covers \Engelsystem\Helpers\Schedule\Event::getTitle
* @covers \Engelsystem\Helpers\Schedule\Event::getSubtitle
* @covers \Engelsystem\Helpers\Schedule\Event::getType
* @covers \Engelsystem\Helpers\Schedule\Event::getDate
* @covers \Engelsystem\Helpers\Schedule\Event::getStart
* @covers \Engelsystem\Helpers\Schedule\Event::getDuration
* @covers \Engelsystem\Helpers\Schedule\Event::getDurationSeconds
* @covers \Engelsystem\Helpers\Schedule\Event::getAbstract
* @covers \Engelsystem\Helpers\Schedule\Event::getSlug
* @covers \Engelsystem\Helpers\Schedule\Event::getTrack
* @covers \Engelsystem\Helpers\Schedule\Event::getLogo
* @covers \Engelsystem\Helpers\Schedule\Event::getPersons
* @covers \Engelsystem\Helpers\Schedule\Event::getLanguage
* @covers \Engelsystem\Helpers\Schedule\Event::getDescription
* @covers \Engelsystem\Helpers\Schedule\Event::getRecording
* @covers \Engelsystem\Helpers\Schedule\Event::getLinks
* @covers \Engelsystem\Helpers\Schedule\Event::getAttachments
* @covers \Engelsystem\Helpers\Schedule\Event::getUrl
* @covers \Engelsystem\Helpers\Schedule\Event::getVideoDownloadUrl
*/
public function testCreateNotDefault()
{
$persons = [1337 => 'Some Person'];
$links = ['https://foo.bar' => 'Foo Bar'];
$attachments = ['/files/foo.pdf' => 'Suspicious PDF'];
$event = new Event(
'3-2-1-0',
2,
new Room('Bar'),
'Lorem',
'Ipsum',
'Workshop',
new Carbon('2021-01-01T00:00:00+00:00'),
'00:00:00',
'00:30',
'Lorem ipsum dolor sit amet',
'2-lorem',
'DevOps',
'/foo/bar.png',
$persons,
'de',
'Foo bar is awesome! & That\'s why...',
'CC BY SA',
$links,
$attachments,
'https://foo.bar/2-lorem',
'https://videos.orem.ipsum/2-lorem.mp4'
);
$this->assertEquals('/foo/bar.png', $event->getLogo());
$this->assertEquals($persons, $event->getPersons());
$this->assertEquals('de', $event->getLanguage());
$this->assertEquals('Foo bar is awesome! & That\'s why...', $event->getDescription());
$this->assertEquals('CC BY SA', $event->getRecording());
$this->assertEquals($links, $event->getLinks());
$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());
}
}

@ -0,0 +1,31 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Test\Unit\TestCase;
class RoomTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\Room::__construct
* @covers \Engelsystem\Helpers\Schedule\Room::getName
* @covers \Engelsystem\Helpers\Schedule\Room::getEvent
* @covers \Engelsystem\Helpers\Schedule\Room::setEvent
*/
public function testCreate()
{
$room = new Room('Test');
$this->assertEquals('Test', $room->getName());
$this->assertEquals([], $room->getEvent());
$events = [$this->createMock(Event::class), $this->createMock(Event::class)];
$events2 = [$this->createMock(Event::class)];
$room = new Room('Test2', $events);
$this->assertEquals($events, $room->getEvent());
$room->setEvent($events2);
$this->assertEquals($events2, $room->getEvent());
}
}

@ -0,0 +1,112 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Carbon\Carbon;
use Engelsystem\Helpers\Schedule\Conference;
use Engelsystem\Helpers\Schedule\Day;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\Schedule;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
class ScheduleTest extends TestCase
{
use HasDatabase;
/**
* @covers \Engelsystem\Helpers\Schedule\Schedule::__construct
* @covers \Engelsystem\Helpers\Schedule\Schedule::getVersion
* @covers \Engelsystem\Helpers\Schedule\Schedule::getConference
* @covers \Engelsystem\Helpers\Schedule\Schedule::getDay
*/
public function testCreate()
{
$conference = new Conference('Foo Bar', 'FooB');
$days = [$this->createMock(Day::class)];
$schedule = new Schedule('Foo\'ing stuff 1.0', $conference, $days);
$this->assertEquals('Foo\'ing stuff 1.0', $schedule->getVersion());
$this->assertEquals($conference, $schedule->getConference());
$this->assertEquals($days, $schedule->getDay());
}
/**
* @covers \Engelsystem\Helpers\Schedule\Schedule::getRooms
*/
public function testGetRooms()
{
$conference = new Conference('Test', 'T');
$room1 = new Room('Test 1');
$room2 = new Room('Test 2');
$room3 = new Room('Test 3');
$days = [
new Day(
'2042-01-01',
new Carbon('2042-01-01T00:00:00+00:00'),
new Carbon('2042-01-01T23:59:00+00:00'),
1,
[$room1, $room2]
),
new Day(
'2042-01-02',
new Carbon('2042-02-01T00:00:00+00:00'),
new Carbon('2042-02-01T23:59:00+00:00'),
2,
[new Room('Test 2'), $room3]
),
];
$schedule = new Schedule('Lorem 1.3.3.7', $conference, $days);
$this->assertEquals(['Test 1' => $room1, 'Test 2' => $room2, 'Test 3' => $room3], $schedule->getRooms());
$schedule = new Schedule('Lorem 1.3.3.0', $conference, []);
$this->assertEquals([], $schedule->getRooms());
}
/**
* @covers \Engelsystem\Helpers\Schedule\Schedule::getStartDateTime
* @covers \Engelsystem\Helpers\Schedule\Schedule::getEndDateTime
*/
public function testGetDateTimes()
{
$conference = new Conference('Some Conference', 'SC');
$days = [
new Day(
'2042-01-02',
new Carbon('2042-01-02T00:00:00+00:00'),
new Carbon('2042-01-02T23:59:00+00:00'),
2
),
new Day(
'2042-01-01',
new Carbon('2042-01-01T00:00:00+00:00'),
new Carbon('2042-01-01T23:59:00+00:00'),
1
),
new Day(
'2042-01-04',
new Carbon('2042-01-04T00:00:00+00:00'),
new Carbon('2042-01-04T23:59:00+00:00'),
3
),
];
$schedule = new Schedule('Ipsum tester', $conference, $days);
$this->assertEquals('2042-01-01T00:00:00+00:00', $schedule->getStartDateTime()->format(Carbon::RFC3339));
$this->assertEquals('2042-01-04T23:59:00+00:00', $schedule->getEndDateTime()->format(Carbon::RFC3339));
$schedule = new Schedule('Ipsum old', $conference, []);
$this->assertNull($schedule->getStartDateTime());
$this->assertNull($schedule->getEndDateTime());
}
/**
* Prepare test
*/
protected function setUp(): void
{
parent::setUp();
$this->initDatabase();
}
}

@ -0,0 +1,54 @@
<?php
namespace Engelsystem\Test\Unit\Helpers\Schedule;
use Engelsystem\Helpers\Schedule\Day;
use Engelsystem\Helpers\Schedule\Event;
use Engelsystem\Helpers\Schedule\Room;
use Engelsystem\Helpers\Schedule\XmlParser;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Support\Arr;
class XmlParserTest extends TestCase
{
/**
* @covers \Engelsystem\Helpers\Schedule\XmlParser::load
* @covers \Engelsystem\Helpers\Schedule\XmlParser::parseXml
* @covers \Engelsystem\Helpers\Schedule\XmlParser::parseEvents
* @covers \Engelsystem\Helpers\Schedule\XmlParser::getFirstXpathContent
* @covers \Engelsystem\Helpers\Schedule\XmlParser::getListFromSequence
* @covers \Engelsystem\Helpers\Schedule\XmlParser::getSchedule
*/
public function testLoad()
{
libxml_use_internal_errors(true);
$parser = new XmlParser();
$this->assertFalse($parser->load('foo'));
$this->assertTrue($parser->load(file_get_contents(__DIR__ . '/Assets/schedule.xml')));
$schedule = $parser->getSchedule();
$this->assertEquals('Some version string', $schedule->getVersion());
$this->assertEquals('Test Event', $schedule->getConference()->getTitle());
/** @var Room $room */
$room = Arr::first($schedule->getRooms());
$this->assertEquals('Rooming', $room->getName());
/** @var Day $day */
$day = Arr::first($schedule->getDay());
$this->assertEquals('2042-01-01', $day->getDate());
$this->assertEquals(1, $day->getIndex());
/** @var Room $room */
$room = Arr::first($day->getRoom());
/** @var Event $event */
$event = Arr::first($room->getEvent());
$this->assertEquals('Foo Bar Test', $event->getTitle());
$this->assertEquals('WTFPL', $event->getRecording());
$this->assertEquals('de', $event->getLanguage());
$this->assertEquals('12:30', $event->getStart());
$this->assertEquals([1234 => 'Some Person'], $event->getPersons());
}
}

@ -0,0 +1,29 @@
<?php
namespace Engelsystem\Test\Unit\Http;
use Engelsystem\Application;
use Engelsystem\Http\GuzzleServiceProvider;
use Engelsystem\Test\Unit\ServiceProviderTest;
use GuzzleHttp\Client as GuzzleClient;
class GuzzleServiceProviderTest extends ServiceProviderTest
{
/**
* @covers \Engelsystem\Http\GuzzleServiceProvider::register
*/
public function testRegister()
{
$app = new Application();
$serviceProvider = new GuzzleServiceProvider($app);
$serviceProvider->register();
/** @var GuzzleClient $guzzle */
$guzzle = $app->make(GuzzleClient::class);
$config = $guzzle->getConfig();
$this->assertFalse($config['http_errors']);
$this->assertArrayHasKey('timeout', $config);
}
}

@ -0,0 +1,41 @@
<?php
namespace Engelsystem\Test\Unit\Models\Shifts;
use Engelsystem\Models\Shifts\Schedule;
use Engelsystem\Models\Shifts\ScheduleShift;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ScheduleShiftTest extends TestCase
{
use HasDatabase;
/**
* @covers \Engelsystem\Models\Shifts\ScheduleShift::schedule
*/
public function testScheduleShifts()
{
$schedule = new Schedule(['url' => 'https://lorem.ipsum/schedule.xml']);
$schedule->save();
$scheduleShift = new ScheduleShift(['shift_id' => 1, 'guid' => 'a']);
$scheduleShift->schedule()->associate($schedule);
$scheduleShift->save();
/** @var ScheduleShift $scheduleShift */
$scheduleShift = (new ScheduleShift())->find(1);
$this->assertInstanceOf(BelongsTo::class, $scheduleShift->schedule());
$this->assertEquals($schedule->id, $scheduleShift->schedule->id);
}
/**
* Prepare test
*/
protected function setUp(): void
{
parent::setUp();
$this->initDatabase();
}
}

@ -0,0 +1,37 @@
<?php
namespace Engelsystem\Test\Unit\Models\Shifts;
use Engelsystem\Models\Shifts\Schedule;
use Engelsystem\Models\Shifts\ScheduleShift;
use Engelsystem\Test\Unit\HasDatabase;
use Engelsystem\Test\Unit\TestCase;
class ScheduleTest extends TestCase
{
use HasDatabase;
/**
* @covers \Engelsystem\Models\Shifts\Schedule::scheduleShifts
*/
public function testScheduleShifts()
{
$schedule = new Schedule(['url' => 'https://foo.bar/schedule.xml']);
$schedule->save();
(new ScheduleShift(['shift_id' => 1, 'schedule_id' => $schedule->id, 'guid' => 'a']))->save();
(new ScheduleShift(['shift_id' => 2, 'schedule_id' => $schedule->id, 'guid' => 'b']))->save();
(new ScheduleShift(['shift_id' => 3, 'schedule_id' => $schedule->id, 'guid' => 'c']))->save();
$this->assertCount(3, $schedule->scheduleShifts);
}
/**
* Prepare test
*/
protected function setUp(): void
{
parent::setUp();
$this->initDatabase();
}
}
Loading…
Cancel
Save