aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/views/consultation/admin/create.php4
-rw-r--r--lib/classes/JsonApi/RouteMap.php4
-rw-r--r--lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php172
-rw-r--r--resources/assets/javascripts/studip-ui.js132
-rw-r--r--resources/assets/stylesheets/studip-jquery-ui.less7
5 files changed, 297 insertions, 22 deletions
diff --git a/app/views/consultation/admin/create.php b/app/views/consultation/admin/create.php
index aa4ee30..ff0b483 100644
--- a/app/views/consultation/admin/create.php
+++ b/app/views/consultation/admin/create.php
@@ -61,7 +61,7 @@ $intervals = [
<input required type="text" name="start-date" id="start-date"
value="<?= htmlReady(Request::get('start-date', strftime('%d.%m.%Y', strtotime('+7 days')))) ?>"
placeholder="<?= _('tt.mm.jjjj') ?>"
- data-date-picker='{">=":"today"}'>
+ data-date-picker='{">=":"today","disable_holidays": true}'>
</label>
<label class="col-3">
@@ -70,7 +70,7 @@ $intervals = [
<input required type="text" name="end-date" id="end-date"
value="<?= htmlReady(Request::get('end-date', strftime('%d.%m.%Y', strtotime('+4 weeks')))) ?>"
placeholder="<?= _('tt.mm.jjjj') ?>"
- data-date-picker='{">=":"#start-date"}'>
+ data-date-picker='{">=":"#start-date","disable_holidays": true}'>
</label>
<label class="col-3">
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 0709ef3..bffa13a 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -7,6 +7,7 @@ use JsonApi\Middlewares\Authentication;
use JsonApi\Middlewares\DangerousRouteHandler;
use JsonApi\Middlewares\JsonApi as JsonApiMiddleware;
use JsonApi\Middlewares\StudipMockNavigation;
+use JsonApi\Routes\Holidays\HolidaysShow;
use Slim\Routing\RouteCollectorProxy;
/**
@@ -143,6 +144,8 @@ class RouteMap
{
\PluginEngine::sendMessage(JsonApiPlugin::class, 'registerUnauthenticatedRoutes', $group);
+ $group->get('/holidays', HolidaysShow::class);
+
$group->get('/semesters', Routes\SemestersIndex::class);
$group->get('/semesters/{id}', Routes\SemestersShow::class)->setName('get-semester');
@@ -561,4 +564,3 @@ class RouteMap
$group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler);
}
}
-
diff --git a/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php b/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php
new file mode 100644
index 0000000..ef3fa61
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace JsonApi\Routes\Holidays;
+
+use GuzzleHttp\Psr7;
+use JsonApi\JsonApiIntegration\QueryParserInterface;
+use JsonApi\NonJsonApiController;
+use Neomerx\JsonApi\Exceptions\JsonApiException;
+use Neomerx\JsonApi\Schema\Error;
+use Neomerx\JsonApi\Schema\ErrorCollection;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+
+/**
+ * List all holidays for a specific time period.
+ *
+ * Filter are allowed for year, month and days. You must specify a year in order
+ * to filter by a month and you must specify a month in order to filter by a
+ * day.
+ *
+ * If no filter is set, a filter the current year is assumed.
+ */
+final class HolidaysShow extends NonJsonApiController
+{
+ private $query_parser;
+
+ protected $allowedFilteringParameters = ['year', 'month', 'day'];
+
+ public function __construct(
+ ContainerInterface $container,
+ QueryParserInterface $queryParser
+ ) {
+ parent::__construct($container);
+
+ $this->query_parser = $queryParser;
+ }
+
+ public function __invoke(Request $request, Response $response, $args): Response
+ {
+ [$current, $end] = $this->getTimespanByFilters();
+
+ $holidays = [];
+ while ($current < $end) {
+ $holiday = holiday($current);
+ if ($holiday) {
+ $holidays[date('Y-m-d', $current)] = [
+ 'holiday' => $holiday['name'],
+ 'mandatory' => $holiday['col'] === 3,
+ ];
+ }
+
+ $current = strtotime('+1 day', $current);
+ }
+
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withBody(Psr7\Utils::streamFor(json_encode($holidays)));
+ }
+
+ private function getTimespanByFilters(): array
+ {
+ $filters = $this->getFilters();
+
+ $begin = mktime(
+ 0,
+ 0,
+ 0,
+ $filters['month'] ?? 1,
+ $filters['day'] ?? 1,
+ $filters['year']
+ );
+
+ $end = mktime(
+ 23,
+ 59,
+ 59,
+ $filters['month'] ?? 12,
+ $filters['day'] ?? $this->getLastDayOfMonth($filters['year'], $filters['month'] ?? 12),
+ $filters['year']
+ );
+
+ return [$begin, $end];
+ }
+
+ private function getLastDayOfMonth(int $year, int $month): int
+ {
+ $first_of_month = mktime(0, 0, 0, $month, 1, $year);
+ $last_day_of_month = strtotime('last day of this month', $first_of_month);
+ return (int) date('d', $last_day_of_month);
+ }
+
+ /**
+ * @todo imporove error handling
+ * @return array
+ */
+ private function getFilters(): array
+ {
+ $errors = new ErrorCollection();
+
+ // Get filters
+ $filters = $this->query_parser->getFilteringParameters();
+
+ // Validate allowed filters
+ foreach ($filters as $key => $value) {
+ if (!in_array($key, $this->allowedFilteringParameters)) {
+ $errors->add(new Error(
+ 'invalid-filter-field',
+ null, null,
+ null, null,
+ 'Filter should contain only allowed values.',
+ "Cannot filter by {$key}",
+ ['filter' => $key]
+ ));
+ }
+ }
+
+ // Validate month
+ if (isset($filters['month']) && !isset($filters['year'])) {
+ $errors->add(new Error(
+ 'missing-year-filter',
+ null, null,
+ null, null,
+ 'You must not define a month filter without a year filter'
+ ));
+ } elseif (
+ isset($filters['month'])
+ && ($filters['month'] < 1 || $filters['month'] > 12)
+ ) {
+ $errors->add(new Error(
+ 'invalid-filter-value',
+ null, null,
+ null, null,
+ 'Filter should contain only allowed values.',
+ "Invalid value {$filters['month']} for month filter",
+ ['filter' => 'month', 'value' => $filters['month']]
+ ));
+ }
+
+ // Validate day
+ if (isset($filters['day']) && !isset($filters['month'])) {
+ $errors->add(new Error(
+ 'missing-month-filter',
+ null, null,
+ null, null,
+ 'You must not define a day filter without a month filter'
+ ));
+ } elseif (
+ isset($filters['day'])
+ && (
+ $filters['day'] < 1
+ || $filters['day'] > $this->getLastDayOfMonth((int) $filters['year'] ?? date('Y'), $filters['month'])
+ )
+ ) {
+ $errors->add(new Error(
+ 'invalid-filter-value',
+ null, null,
+ null, null,
+ 'Filter should contain only allowed values.',
+ "Invalid value {$filters['day']} for day filter",
+ ['filter' => 'day', 'value' => $filters['day']]
+ ));
+ }
+
+ if ($errors->count() > 0) {
+ throw new JsonApiException($errors, JsonApiException::HTTP_CODE_BAD_REQUEST);
+ }
+
+ // Apply defaults
+ return array_merge(['year' => date('Y')], $filters);
+ }
+}
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
index 41897b2..63766c0 100644
--- a/resources/assets/javascripts/studip-ui.js
+++ b/resources/assets/javascripts/studip-ui.js
@@ -27,6 +27,37 @@ import eventBus from "./lib/event-bus.ts";
return element?.classList?.contains('ck-body-wrapper');
}
+ function disableHolidaysBeforeShow(date) {
+ const year = date.getFullYear();
+
+ if (STUDIP.UI.restrictedDates[year] === undefined) {
+ STUDIP.UI.restrictedDates[year] = {};
+
+ STUDIP.jsonapi.GET('holidays', {data: {
+ 'filter[year]': year
+ }}).done(response => {
+ // Since PHP will return an empty object as an array,
+ // we need to check
+ if (Array.isArray(response)) {
+ return;
+ }
+
+ for (const [date, data] of Object.entries(response)) {
+ STUDIP.UI.addRestrictedDate(
+ new Date(date),
+ data.holiday,
+ data.mandatory
+ );
+ }
+
+ $(this).datepicker('refresh');
+ });
+ }
+
+ const {reason, lock} = STUDIP.UI.isDateRestricted(date, false);
+ return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason];
+ }
+
/**
* Setup and refine date picker, add automated handling for .has-date-picker
* and [data-date-picker].
@@ -53,27 +84,84 @@ import eventBus from "./lib/event-bus.ts";
}
// Setup Stud.IP's own datepicker extensions
- STUDIP.UI = STUDIP.UI || {};
+ STUDIP.UI = Object.assign(STUDIP.UI || {}, {
+ restrictedDates: {},
+ addRestrictedDate(date, reason = '', lock = true) {
+ if (this.isDateRestricted(date)) {
+ return;
+ }
+
+ const [year, month, day] = this.convertDateForRestriction(date);
+ if (this.restrictedDates[year] === undefined) {
+ this.restrictedDates[year] = {};
+ }
+ if (this.restrictedDates[year][month] === undefined) {
+ this.restrictedDates[year][month] = {};
+ }
+
+ this.restrictedDates[year][month][day] = {reason, lock};
+ },
+ removeRestrictedDate(date) {
+ if (!this.isDateRestricted(date)) {
+ return false;
+ }
+ const [year, month, day] = this.convertDateForRestriction(date);
+
+ delete this.restrictedDates[year][month][day];
+
+ if (Object.keys(this.restrictedDates[year][month]).length === 0) {
+ delete this.restrictedDates[year][month];
+ }
+
+ return true;
+ },
+ isDateRestricted(date, return_bool = true) {
+ const [year, month, day] = this.convertDateForRestriction(date);
+ if (
+ this.restrictedDates[year] === undefined
+ || this.restrictedDates[year][month] === undefined
+ || this.restrictedDates[year][month][day] === undefined
+ ) {
+ return return_bool ? false : {
+ reason: null,
+ lock: false,
+ };
+ }
+
+ return return_bool ? true : this.restrictedDates[year][month][day];
+ },
+ convertDateForRestriction(date) {
+ return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
+ }
+ });
STUDIP.UI.Datepicker = {
selector: '.has-date-picker,[data-date-picker]',
// Initialize all datepickers that not yet been initialized (e.g. in dialogs)
- init: function () {
+ init() {
$(this.selector).filter(function () {
return $(this).data('date-picker-init') === undefined;
}).each(function () {
- $(this).data('date-picker-init', true).datepicker();
+ const dataOptions = $(this).data().datePicker;
+
+ const options = {};
+ if (
+ dataOptions['disable_holidays'] !== undefined
+ && dataOptions['disable_holidays'] === true
+ ) {
+ options.beforeShowDay = disableHolidaysBeforeShow;
+ }
+ $(this).data('date-picker-init', true).datepicker(options);
});
},
// Apply registered handlers. Take care: This happens upon before a
// picker is shown as well as after a date has been selected.
- refresh: function () {
+ refresh() {
$(this.selector).each(function () {
- var element = this,
- options = $(element).data().datePicker;
+ const options = $(this).data().datePicker;
if (options) {
- $.each(options, function (key, value) {
+ $.each(options, (key, value) => {
if (STUDIP.UI.Datepicker.dataHandlers[key] !== undefined) {
- STUDIP.UI.Datepicker.dataHandlers[key].call(element, value);
+ STUDIP.UI.Datepicker.dataHandlers[key].call(this, value);
}
});
}
@@ -190,23 +278,31 @@ import eventBus from "./lib/event-bus.ts";
STUDIP.UI.DateTimepicker = {
selector: '.has-datetime-picker,[data-datetime-picker]',
// Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
- init: function () {
+ init() {
$(this.selector).filter(function () {
return $(this).data('datetime-picker-init') === undefined;
}).each(function () {
- $(this).data('datetime-picker-init', true).datetimepicker();
+ const dataOptions = $(this).data().datePicker;
+
+ const options = {};
+ if (
+ dataOptions['disable_holidays'] !== undefined
+ && dataOptions['disable_holidays'] === true
+ ) {
+ options.beforeShowDay = disableHolidaysBeforeShow;
+ }
+ $(this).data('date-picker-init', true).datepicker(options);
});
},
// Apply registered handlers. Take care: This happens upon before a
// picker is shown as well as after a date has been selected.
- refresh: function () {
+ refresh() {
$(this.selector).each(function () {
- var element = this,
- options = $(element).data().datetimePicker;
+ const options = $(this).data().datetimePicker;
if (options) {
- $.each(options, function (key, value) {
+ $.each(options, (key, value) => {
if (STUDIP.UI.DateTimepicker.dataHandlers[key] !== undefined) {
- STUDIP.UI.DateTimepicker.dataHandlers[key].call(element, value);
+ STUDIP.UI.DateTimepicker.dataHandlers[key].call(this, value);
}
});
}
@@ -317,7 +413,7 @@ import eventBus from "./lib/event-bus.ts";
STUDIP.UI.Timepicker = {
selector: '.has-time-picker,[data-time-picker]',
// Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
- init: function () {
+ init() {
$(this.selector).filter(function () {
return $(this).data('time-picker-init') === undefined;
}).each(function () {
@@ -326,7 +422,7 @@ import eventBus from "./lib/event-bus.ts";
},
// Apply registered handlers. Take care: This happens upon before a
// picker is shown as well as after a date has been selected.
- refresh: function () {
+ refresh() {
$(this.selector).each(function () {
var element = this,
options = $(element).data().timePicker;
@@ -395,7 +491,6 @@ import eventBus from "./lib/event-bus.ts";
parsed.minute
);
- console.log('max time:', this_time, max_time);
if (this_time && this_time > max_time) {
$(this).timepicker(STUDIP.UI.Timepicker.parseTime(max_time));
}
@@ -446,7 +541,6 @@ import eventBus from "./lib/event-bus.ts";
parsed.minute
);
- console.log('min time:', this_time, min_time);
if (this_time && this_time < min_time) {
$(this).timepicker(STUDIP.UI.Timepicker.parseTime(min_time));
}
diff --git a/resources/assets/stylesheets/studip-jquery-ui.less b/resources/assets/stylesheets/studip-jquery-ui.less
index c9fb6de..2303779 100644
--- a/resources/assets/stylesheets/studip-jquery-ui.less
+++ b/resources/assets/stylesheets/studip-jquery-ui.less
@@ -176,3 +176,10 @@
.ui-menu .ui-menu-item {
list-style: none;
}
+
+.ui-datepicker-calendar {
+ // This will reenable the tooltip
+ .ui-datepicker-unselectable.ui-datepicker-is-locked {
+ pointer-events: all;
+ }
+}