Skip to content
Snippets Groups Projects
Commit 85588e61 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms Committed by David Siegfried
Browse files

introduce datepicker/datetimepicker 'disable_holidays: true' to disable...

introduce datepicker/datetimepicker 'disable_holidays: true' to disable holidays in the calendar, fixes #2267

Closes #2267

Merge request studip/studip!1632
parent d9f6695c
No related branches found
No related tags found
No related merge requests found
......@@ -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">
......
......@@ -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);
}
}
<?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);
}
}
......@@ -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));
}
......
......@@ -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;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment