Reimplemented shifts ical export
This commit is contained in:
parent
3d0d5067fd
commit
b0b4cb54ec
|
@ -98,6 +98,7 @@ $route->get('/api[/{resource:.+}]', 'ApiController@index');
|
|||
|
||||
// Feeds
|
||||
$route->get('/atom', 'FeedController@atom');
|
||||
$route->get('/ical', 'FeedController@ical');
|
||||
$route->get('/rss', 'FeedController@rss');
|
||||
|
||||
// Design
|
||||
|
|
|
@ -9,7 +9,6 @@ use Engelsystem\Models\Shifts\Shift;
|
|||
use Engelsystem\Models\Shifts\ShiftType;
|
||||
use Engelsystem\Models\Shifts\ShiftSignupStatus;
|
||||
use Engelsystem\ShiftSignupState;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @param array|Shift $shift
|
||||
|
@ -382,7 +381,7 @@ function shift_next_controller()
|
|||
|
||||
/**
|
||||
* Export filtered shifts via JSON.
|
||||
* (Like iCal Export or shifts view)
|
||||
* (Like shifts view)
|
||||
*/
|
||||
function shifts_json_export_controller()
|
||||
{
|
||||
|
@ -396,7 +395,7 @@ function shifts_json_export_controller()
|
|||
throw new HttpForbidden('{"error":"Not allowed"}', ['content-type' => 'application/json']);
|
||||
}
|
||||
|
||||
$shifts = load_ical_shifts();
|
||||
$shifts = Shifts_by_user(auth()->user()->id);
|
||||
$shifts->sortBy('start_date');
|
||||
$timeZone = CarbonTimeZone::create(config('timezone'));
|
||||
|
||||
|
@ -460,13 +459,3 @@ function shifts_json_export_controller()
|
|||
header('Content-Type: application/json; charset=utf-8');
|
||||
raw_output(json_encode($shiftsData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns users shifts to export.
|
||||
*
|
||||
* @return Shift[]|Collection
|
||||
*/
|
||||
function load_ical_shifts()
|
||||
{
|
||||
return Shifts_by_user(auth()->user()->id);
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Engelsystem\Http\Exceptions\HttpForbidden;
|
||||
use Engelsystem\Models\Shifts\Shift;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Controller for ical output of users own shifts or any user_shifts filter.
|
||||
*/
|
||||
function user_ical()
|
||||
{
|
||||
$user = auth()->userFromApi();
|
||||
|
||||
if (!$user) {
|
||||
throw new HttpForbidden('Missing or invalid ?key=', ['content-type' => 'text/text']);
|
||||
}
|
||||
|
||||
if (!auth()->can('ical')) {
|
||||
throw new HttpForbidden('Not allowed', ['content-type' => 'text/text']);
|
||||
}
|
||||
|
||||
$ical_shifts = load_ical_shifts();
|
||||
|
||||
send_ical_from_shifts($ical_shifts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an ical calendar from given shifts array.
|
||||
*
|
||||
* @param Shift[]|Collection $shifts Shift
|
||||
*/
|
||||
function send_ical_from_shifts($shifts)
|
||||
{
|
||||
header('Content-Type: text/calendar; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename=shifts.ics');
|
||||
$output = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//-//" . config('app_name') . "//DE\r\nCALSCALE:GREGORIAN\r\n";
|
||||
foreach ($shifts as $shift) {
|
||||
$output .= make_ical_entry_from_shift($shift);
|
||||
}
|
||||
$output .= "END:VCALENDAR\r\n";
|
||||
$output = trim($output, "\x0A");
|
||||
header('Content-Length: ' . strlen($output));
|
||||
raw_output($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an ical vevent from given shift.
|
||||
*
|
||||
* @param Shift $shift
|
||||
* @return string
|
||||
*/
|
||||
function make_ical_entry_from_shift(Shift $shift)
|
||||
{
|
||||
$output = "BEGIN:VEVENT\r\n";
|
||||
$output .= 'UID:' . md5($shift->start->timestamp . $shift->end->timestamp . $shift->shiftType->name) . "\r\n";
|
||||
$output .= 'SUMMARY:' . str_replace("\n", "\\n", $shift->shiftType->name)
|
||||
. ' (' . str_replace("\n", "\\n", $shift->title) . ")\r\n";
|
||||
if (isset($shift->user_comment)) {
|
||||
$output .= 'DESCRIPTION:' . str_replace("\n", "\\n", $shift->user_comment) . "\r\n";
|
||||
}
|
||||
$output .= 'DTSTAMP:' . $shift->start->utc()->format('Ymd\THis\Z') . "\r\n";
|
||||
$output .= 'DTSTART:' . $shift->start->utc()->format('Ymd\THis\Z') . "\r\n";
|
||||
$output .= 'DTEND:' . $shift->end->utc()->format('Ymd\THis\Z') . "\r\n";
|
||||
$output .= 'LOCATION:' . $shift->room->name . "\r\n";
|
||||
$output .= "END:VEVENT\r\n";
|
||||
return $output;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{% set dateFormat = 'Ymd\\THis\\Z' %}
|
||||
{% set replacement = {'\n': '\\n'} %}
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//-/{{ config('app_name') }}//DE
|
||||
CALSCALE:GREGORIAN
|
||||
{% for entry in shiftEntries %}
|
||||
BEGIN:VEVENT
|
||||
UID:{{ uuidBy(entry.id, '54117') }}
|
||||
DTSTAMP:{{ entry.shift.start.utc().format(dateFormat) }}
|
||||
DTSTART:{{ entry.shift.start.utc().format(dateFormat) }}
|
||||
DTEND:{{ entry.shift.end.utc().format(dateFormat) }}
|
||||
STATUS:CONFIRMED
|
||||
TRANSP:OPAQUE
|
||||
SUMMARY:{{ entry.shift.shiftType.name ~ ' (' ~ entry.shift.title ~ ')' | replace(replacement) | raw }}
|
||||
LOCATION:{{ entry.shift.room.name | replace(replacement) | raw }}
|
||||
DESCRIPTION:{{
|
||||
entry.shift.shiftType.description
|
||||
~ '\\n' ~ entry.shift.description
|
||||
~ '\\n' ~ entry.user_comment
|
||||
| replace(replacement) | raw
|
||||
}}
|
||||
URL:{{ url('/shifts', {'action': 'view', 'shift_id': entry.shift.id}) | raw }}
|
||||
END:VEVENT
|
||||
{% endfor %}
|
||||
END:VCALENDAR
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Engelsystem\Controllers;
|
||||
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Http\Request;
|
||||
use Engelsystem\Http\Response;
|
||||
use Engelsystem\Models\News;
|
||||
|
@ -13,9 +14,11 @@ class FeedController extends BaseController
|
|||
protected array $permissions = [
|
||||
'atom' => 'atom',
|
||||
'rss' => 'atom',
|
||||
'ical' => 'ical',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Authenticator $auth,
|
||||
protected Request $request,
|
||||
protected Response $response,
|
||||
) {
|
||||
|
@ -39,6 +42,16 @@ class FeedController extends BaseController
|
|||
->withView('api/rss', ['news' => $news]);
|
||||
}
|
||||
|
||||
public function ical(): Response
|
||||
{
|
||||
$shifts = $this->getShifts();
|
||||
|
||||
return $this->response
|
||||
->withHeader('content-type', 'text/calendar; charset=utf-8')
|
||||
->withHeader('content-disposition', 'attachment; filename=shifts.ics')
|
||||
->withView('api/ical', ['shiftEntries' => $shifts]);
|
||||
}
|
||||
|
||||
protected function getNews(): Collection
|
||||
{
|
||||
$news = $this->request->has('meetings')
|
||||
|
@ -50,4 +63,14 @@ class FeedController extends BaseController
|
|||
|
||||
return $news->get();
|
||||
}
|
||||
|
||||
protected function getShifts(): Collection
|
||||
{
|
||||
return $this->auth->userFromApi()
|
||||
->shiftEntries()
|
||||
->leftJoin('shifts', 'shifts.id', 'shift_entries.shift_id')
|
||||
->orderBy('shifts.start')
|
||||
->with(['shift', 'shift.room', 'shift.shiftType'])
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ class LegacyMiddleware implements MiddlewareInterface
|
|||
protected array $free_pages = [
|
||||
'admin_event_config',
|
||||
'angeltypes',
|
||||
'ical',
|
||||
'public_dashboard',
|
||||
'rooms',
|
||||
'shift_entries',
|
||||
|
@ -78,10 +77,6 @@ class LegacyMiddleware implements MiddlewareInterface
|
|||
protected function loadPage(string $page): array
|
||||
{
|
||||
switch ($page) {
|
||||
case 'ical':
|
||||
require_once realpath(__DIR__ . '/../../includes/pages/user_ical.php');
|
||||
user_ical();
|
||||
break;
|
||||
case 'shifts_json_export':
|
||||
require_once realpath(__DIR__ . '/../../includes/controller/shifts_controller.php');
|
||||
shifts_json_export_controller();
|
||||
|
|
|
@ -3,19 +3,27 @@
|
|||
namespace Engelsystem\Test\Unit\Controllers;
|
||||
|
||||
use Engelsystem\Controllers\FeedController;
|
||||
use Engelsystem\Helpers\Authenticator;
|
||||
use Engelsystem\Helpers\Carbon;
|
||||
use Engelsystem\Models\News;
|
||||
use Engelsystem\Models\Shifts\ShiftEntry;
|
||||
use Engelsystem\Models\User\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
|
||||
class FeedControllerTest extends ControllerTest
|
||||
{
|
||||
protected Authenticator|MockObject $auth;
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::__construct
|
||||
* @covers \Engelsystem\Controllers\FeedController::atom
|
||||
*/
|
||||
public function testAtom(): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||
|
||||
$this->setExpects(
|
||||
$this->response,
|
||||
|
@ -40,7 +48,7 @@ class FeedControllerTest extends ControllerTest
|
|||
*/
|
||||
public function testRss(): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||
|
||||
$this->setExpects(
|
||||
$this->response,
|
||||
|
@ -59,6 +67,50 @@ class FeedControllerTest extends ControllerTest
|
|||
$controller->rss();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Engelsystem\Controllers\FeedController::ical
|
||||
* @covers \Engelsystem\Controllers\FeedController::getShifts
|
||||
*/
|
||||
public function testIcal(): void
|
||||
{
|
||||
$this->request = $this->request->withQueryParams(['key' => 'fo0']);
|
||||
$this->auth = new Authenticator(
|
||||
$this->request,
|
||||
new Session(new MockArraySessionStorage()),
|
||||
new User(),
|
||||
);
|
||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create(['api_key' => 'fo0']);
|
||||
ShiftEntry::factory(3)->create(['user_id' => $user->id]);
|
||||
|
||||
$this->response->expects($this->exactly(2))
|
||||
->method('withHeader')
|
||||
->withConsecutive(
|
||||
['content-type', 'text/calendar; charset=utf-8'],
|
||||
['content-disposition', 'attachment; filename=shifts.ics']
|
||||
)
|
||||
->willReturn($this->response);
|
||||
|
||||
$this->response->expects($this->once())
|
||||
->method('withView')
|
||||
->willReturnCallback(function ($view, $data) {
|
||||
$this->assertEquals('api/ical', $view);
|
||||
$this->assertArrayHasKey('shiftEntries', $data);
|
||||
|
||||
/** @var ShiftEntry[]|Collection $shiftEntries */
|
||||
$shiftEntries = $data['shiftEntries'];
|
||||
$this->assertCount(3, $shiftEntries);
|
||||
|
||||
$this->assertTrue($shiftEntries[0]->shift->start < $shiftEntries[1]->shift->start);
|
||||
|
||||
return $this->response;
|
||||
});
|
||||
$controller->ical();
|
||||
}
|
||||
|
||||
|
||||
public function getNewsMeetingsDataProvider(): array
|
||||
{
|
||||
return [
|
||||
|
@ -73,7 +125,7 @@ class FeedControllerTest extends ControllerTest
|
|||
*/
|
||||
public function testGetNewsMeetings(bool $isMeeting): void
|
||||
{
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||
|
||||
$this->request->attributes->set('meetings', $isMeeting);
|
||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||
|
@ -100,7 +152,7 @@ class FeedControllerTest extends ControllerTest
|
|||
public function testGetNewsLimit(): void
|
||||
{
|
||||
News::query()->where('id', '<>', 1)->update(['updated_at' => Carbon::now()->subHour()]);
|
||||
$controller = new FeedController($this->request, $this->response);
|
||||
$controller = new FeedController($this->auth, $this->request, $this->response);
|
||||
|
||||
$this->setExpects($this->response, 'withHeader', null, $this->response);
|
||||
$this->response->expects($this->once())
|
||||
|
@ -125,6 +177,7 @@ class FeedControllerTest extends ControllerTest
|
|||
parent::setUp();
|
||||
|
||||
$this->config->set('display_news', 10);
|
||||
$this->auth = $this->createMock(Authenticator::class);
|
||||
|
||||
News::factory(7)->create(['is_meeting' => false]);
|
||||
News::factory(5)->create(['is_meeting' => true]);
|
||||
|
|
Loading…
Reference in New Issue