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

lock times for consultation blocks, fixes #1264

Closes #1264

Merge request studip/studip!785
parent de69b6fd
No related branches found
No related tags found
No related merge requests found
Showing
with 189 additions and 20 deletions
...@@ -199,6 +199,7 @@ class Consultation_AdminController extends ConsultationController ...@@ -199,6 +199,7 @@ class Consultation_AdminController extends ConsultationController
$block->confirmation_text = trim(Request::get('confirmation-text')) ?: null; $block->confirmation_text = trim(Request::get('confirmation-text')) ?: null;
$block->note = Request::get('note'); $block->note = Request::get('note');
$block->size = Request::int('size', 1); $block->size = Request::int('size', 1);
$block->lock_time = Request::int('lock_time');
$slots = $block->createSlots(Request::int('duration'), $pause_time, $pause_duration); $slots = $block->createSlots(Request::int('duration'), $pause_time, $pause_duration);
if (count($slots) === 0) { if (count($slots) === 0) {
...@@ -387,6 +388,7 @@ class Consultation_AdminController extends ConsultationController ...@@ -387,6 +388,7 @@ class Consultation_AdminController extends ConsultationController
$this->block->show_participants = Request::bool('show-participants', false); $this->block->show_participants = Request::bool('show-participants', false);
$this->block->require_reason = Request::option('require-reason'); $this->block->require_reason = Request::option('require-reason');
$this->block->confirmation_text = trim(Request::get('confirmation-text')); $this->block->confirmation_text = trim(Request::get('confirmation-text'));
$this->block->lock_time = Request::int('lock_time');
foreach ($this->block->slots as $slot) { foreach ($this->block->slots as $slot) {
$slot->note = ''; $slot->note = '';
......
...@@ -140,6 +140,18 @@ $intervals = [ ...@@ -140,6 +140,18 @@ $intervals = [
value="<?= Request::int('pause_duration', 15) ?>" value="<?= Request::int('pause_duration', 15) ?>"
min="1"> min="1">
</label> </label>
<label>
<input type="checkbox" name="lock" value="1" data-shows=".lock-inputs" data-activates=".lock-inputs input">
<?= _('Termine für Buchungen sperren?') ?>
</label>
<label class="lock-inputs">
<?= _('Wieviele Stunden vor Beginn des Blocks sollen die Termine für Buchungen gesperrt werden?') ?>
<input type="number" name="lock_time"
value="<?= htmlReady(Request::int('lock_time', 24)) ?>"
min="1">
</label>
</fieldset> </fieldset>
<? if ($responsible): ?> <? if ($responsible): ?>
......
...@@ -27,6 +27,19 @@ ...@@ -27,6 +27,19 @@
<textarea name="note"><?= htmlReady($block->note ) ?></textarea> <textarea name="note"><?= htmlReady($block->note ) ?></textarea>
</label> </label>
<label>
<input type="checkbox" name="lock" value="1" data-shows=".lock-inputs" data-activates=".lock-inputs input"
<? if ($block->lock_time) echo 'checked'; ?>>
<?= _('Termine für Buchungen sperren?') ?>
</label>
<label class="lock-inputs">
<?= _('Wieviele Stunden vor Beginn des Blocks sollen die Termine für Buchungen gesperrt werden?') ?>
<input type="number" name="lock_time"
value="<?= htmlReady($block->lock_time) ?>"
min="1">
</label>
<? if ($responsible): ?> <? if ($responsible): ?>
<?= $this->render_partial('consultation/admin/block-responsibilities.php', compact('responsible', 'block')) ?> <?= $this->render_partial('consultation/admin/block-responsibilities.php', compact('responsible', 'block')) ?>
<? endif; ?> <? endif; ?>
......
...@@ -24,11 +24,11 @@ ...@@ -24,11 +24,11 @@
<form action="<?= $controller->bulk($page, $current_action === 'expired') ?>" method="post"> <form action="<?= $controller->bulk($page, $current_action === 'expired') ?>" method="post">
<table class="default consultation-overview"> <table class="default consultation-overview">
<colgroup> <colgroup>
<col width="24px"> <col style="width: 24px">
<col width="10%"> <col style="width: 10%">
<col width="12%"> <col style="width: 12%">
<col> <col>
<col width="48px"> <col style="width: 48px">
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
<th class="actions"> <th class="actions">
<?= ActionMenu::get()->setContext(strval($block['block']))->addLink( <?= ActionMenu::get()->setContext(strval($block['block']))->addLink(
$controller->editURL($block['block'], 0, $page), $controller->editURL($block['block'], 0, $page),
_('Bearbeiten'), _('Block bearbeiten'),
Icon::create('edit'), Icon::create('edit'),
['data-dialog' => 'size=auto'] ['data-dialog' => 'size=auto']
)->addLink( )->addLink(
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
date('H:i', $block->start), date('H:i', $block->start),
date('H:i', $block->end) date('H:i', $block->end)
) ?> ) ?>
<?= $this->render_partial('consultation/block-locked.php', compact('block')) ?>
</td> </td>
<td> <td>
<? if (count($block->responsibilities) > 0): ?> <? if (count($block->responsibilities) > 0): ?>
...@@ -83,7 +84,7 @@ ...@@ -83,7 +84,7 @@
<td class="actions"> <td class="actions">
<?= ActionMenu::get()->setContext(strval($block))->addLink( <?= ActionMenu::get()->setContext(strval($block))->addLink(
$controller->editURL($block, 0, $page), $controller->editURL($block, 0, $page),
_('Information bearbeiten'), _('Block bearbeiten'),
Icon::create('edit'), Icon::create('edit'),
['data-dialog' => 'size=auto'] ['data-dialog' => 'size=auto']
)->addLink( )->addLink(
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
date('H:i', $block->end) date('H:i', $block->end)
) ?> ) ?>
<?= $this->render_partial('consultation/block-locked.php', compact('block')) ?>
(<?= formatLinks($block->room) ?>) (<?= formatLinks($block->room) ?>)
<? if ($block->show_participants): ?> <? if ($block->show_participants): ?>
......
<? if ($block->lock_time): ?>
<?= tooltipIcon(sprintf(
_('Dieser Block wird %u Stunden vor Beginn für Buchungen gesperrt.'),
$block->lock_time
)) ?>
<? endif; ?>
<?php
/**
* @var ConsultationBlock[] $blocks
* @var Consultation_OverviewController $controller
* @var int $count
* @var int $limit
* @var int $page
*
* @var callable $displayNote
*/
?>
<? if (count($blocks) === 0): ?> <? if (count($blocks) === 0): ?>
<?= MessageBox::info(_('Aktuell werden keine Termine angeboten.'))->hideClose() ?> <?= MessageBox::info(_('Aktuell werden keine Termine angeboten.'))->hideClose() ?>
...@@ -6,10 +17,10 @@ ...@@ -6,10 +17,10 @@
<table class="default"> <table class="default">
<colgroup> <colgroup>
<col width="10%"> <col style="width: 10%">
<col width="10%"> <col style="width: 10%">
<col> <col>
<col width="24px"> <col style="width: 24px">
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
...@@ -45,10 +56,12 @@ ...@@ -45,10 +56,12 @@
<a href="<?= $controller->cancel($block, $slot) ?>" data-dialog="size=auto"> <a href="<?= $controller->cancel($block, $slot) ?>" data-dialog="size=auto">
<?= Icon::create('trash')->asImg(tooltip2(_('Termin absagen'))) ?> <?= Icon::create('trash')->asImg(tooltip2(_('Termin absagen'))) ?>
</a> </a>
<? elseif (!$slot->isOccupied()): ?> <? elseif ($slot->userMayCreateBookingForSlot()): ?>
<a href="<?= $controller->book($block, $slot) ?>" data-dialog="size=auto"> <a href="<?= $controller->book($block, $slot) ?>" data-dialog="size=auto">
<?= Icon::create('add')->asImg(tooltip2(_('Termin reservieren'))) ?> <?= Icon::create('add')->asImg(tooltip2(_('Termin reservieren'))) ?>
</a> </a>
<? else: ?>
<?= Icon::create('add', Icon::ROLE_INACTIVE)->asImg(tooltip2(_('Dieser Termin ist für Buchungen gesperrt.'))) ?>
<? endif; ?> <? endif; ?>
</td> </td>
</tr> </tr>
......
...@@ -73,12 +73,14 @@ ...@@ -73,12 +73,14 @@
<td class="actions"> <td class="actions">
<? if ($slot->isOccupied($GLOBALS['user']->id)): ?> <? if ($slot->isOccupied($GLOBALS['user']->id)): ?>
<a href="<?= $controller->cancel($block, $slot) ?>" data-dialog="size=auto"> <a href="<?= $controller->cancel($block, $slot) ?>" data-dialog="size=auto">
<?= Icon::create('remove/consultation')->asImg(tooltip2(_('Termin absagen'))) ?> <?= Icon::create('trash')->asImg(tooltip2(_('Termin absagen'))) ?>
</a> </a>
<? elseif (!$slot->isOccupied()): ?> <? elseif ($slot->userMayCreateBookingForSlot()): ?>
<a href="<?= $controller->book($block, $slot) ?>" data-dialog="size=auto"> <a href="<?= $controller->book($block, $slot) ?>" data-dialog="size=auto">
<?= Icon::create('add/consultation')->asImg(tooltip2(_('Termin reservieren'))) ?> <?= Icon::create('add')->asImg(tooltip2(_('Termin reservieren'))) ?>
</a> </a>
<? else: ?>
<?= Icon::create('add', Icon::ROLE_INACTIVE)->asImg(tooltip2(_('Dieser Termin ist für Buchungen gesperrt.'))) ?>
<? endif; ?> <? endif; ?>
</td> </td>
</tr> </tr>
......
<?php
/**
* @var ConsultationSlot $slot
*/
?>
<? if ($slot->isOccupied()): ?> <? if ($slot->isOccupied()): ?>
<span class="consultation-occupied"> <span class="consultation-occupied">
<? if ($slot->block->size > 1): ?> <? if ($slot->block->size > 1): ?>
...@@ -12,6 +17,10 @@ ...@@ -12,6 +17,10 @@
<?= _('belegt') ?> <?= _('belegt') ?>
<? endif; ?> <? endif; ?>
</span> </span>
<? elseif ($slot->isLocked()): ?>
<span class="consultation-slot-not-bookable">
<?= _('nicht buchbar') ?>
</span>
<? else: ?> <? else: ?>
<span class="consultation-free"> <span class="consultation-free">
<? if ($slot->block->size > 1): ?> <? if ($slot->block->size > 1): ?>
......
<?php
final class AddConsultationLockTime extends Migration
{
public function description()
{
return 'Adds a lock time for consultation blocks that prevents slots '
. 'from being booked based on the current time.';
}
protected function up()
{
$query = "ALTER TABLE `consultation_blocks`
ADD COLUMN `lock_time` INT(11) UNSIGNED DEFAULT NULL AFTER `size`";
DBManager::get()->exec($query);
}
protected function down()
{
$query = "ALTER TABLE `consultation_blocks`
DROP COLUMN `lock_time`";
DBManager::get()->exec($query);
}
}
...@@ -29,6 +29,10 @@ class BookingsCreate extends JsonApiController ...@@ -29,6 +29,10 @@ class BookingsCreate extends JsonApiController
throw new ConflictException('The slot is already occupied'); throw new ConflictException('The slot is already occupied');
} }
if (!$slot->userMayCreateBookingForSlot($booking_user)) {
throw new ConflictException('The slot is locked for bookings');
}
$booking = \ConsultationBooking::create([ $booking = \ConsultationBooking::create([
'slot_id' => $slot->id, 'slot_id' => $slot->id,
'user_id' => $booking_user->id, 'user_id' => $booking_user->id,
......
...@@ -26,6 +26,8 @@ class ConsultationBlock extends SchemaProvider ...@@ -26,6 +26,8 @@ class ConsultationBlock extends SchemaProvider
'start' => date('c', $resource->start), 'start' => date('c', $resource->start),
'end' => date('c', $resource->end), 'end' => date('c', $resource->end),
'lock-time' => $resource->lock_time,
'size' => (int) $resource->size, 'size' => (int) $resource->size,
'show-participants' => (bool) $resource->show_participants, 'show-participants' => (bool) $resource->show_participants,
'require-reason' => $resource->require_reason, 'require-reason' => $resource->require_reason,
......
...@@ -17,6 +17,12 @@ class ConsultationSlot extends SchemaProvider ...@@ -17,6 +17,12 @@ class ConsultationSlot extends SchemaProvider
return $resource->id; return $resource->id;
} }
/**
* @param \ConsultationSlot $resource
* @param ContextInterface $context
*
* @return iterable
*/
public function getAttributes($resource, ContextInterface $context): iterable public function getAttributes($resource, ContextInterface $context): iterable
{ {
$attributes = [ $attributes = [
...@@ -26,6 +32,9 @@ class ConsultationSlot extends SchemaProvider ...@@ -26,6 +32,9 @@ class ConsultationSlot extends SchemaProvider
'start_time' => date('c', $resource->start_time), 'start_time' => date('c', $resource->start_time),
'end_time' => date('c', $resource->end_time), 'end_time' => date('c', $resource->end_time),
'is-bookable' => !$resource->isOccupied() && !$resource->isLocked(),
'is-locked' => $resource->isLocked(),
'mkdate' => date('c', $resource->mkdate), 'mkdate' => date('c', $resource->mkdate),
'chdate' => date('c', $resource->chdate), 'chdate' => date('c', $resource->chdate),
]; ];
......
...@@ -23,10 +23,13 @@ ...@@ -23,10 +23,13 @@
* @property string $confirmation_text database column * @property string $confirmation_text database column
* @property string $note database column * @property string $note database column
* @property string $size database column * @property string $size database column
* @property int $lock_time
* @property int $mkdate database column * @property int $mkdate database column
* @property int $chdate database column * @property int $chdate database column
* *
* @property bool $has_bookings computed column * @property bool $has_bookings computed column
* @property string $range_display
* @property bool $is_expired
* @property Range $range computed column * @property Range $range computed column
* @property ConsultationSlot[]|SimpleORMapCollection $slots has_many ConsultationSlot * @property ConsultationSlot[]|SimpleORMapCollection $slots has_many ConsultationSlot
* @property ConsultationResponsibility[]|SimpleCollection $responsibilities has_many ConsultationResponsibility * @property ConsultationResponsibility[]|SimpleCollection $responsibilities has_many ConsultationResponsibility
......
...@@ -146,6 +146,17 @@ class ConsultationSlot extends SimpleORMap ...@@ -146,6 +146,17 @@ class ConsultationSlot extends SimpleORMap
: (bool) $this->bookings->findOneBy('user_id', $user_id); : (bool) $this->bookings->findOneBy('user_id', $user_id);
} }
/**
* Returns whether the slot is locked for bookings.
*
* @return bool
*/
public function isLocked(): bool
{
return $this->block->lock_time
&& strtotime("-{$this->block->lock_time} hours", $this->block->start) < time();
}
/** /**
* Creates a Stud.IP calendar event relating to the slot. * Creates a Stud.IP calendar event relating to the slot.
* *
...@@ -284,6 +295,18 @@ class ConsultationSlot extends SimpleORMap ...@@ -284,6 +295,18 @@ class ConsultationSlot extends SimpleORMap
} }
} }
/**
* Returns whether the given user may create a booking for this slot.
*/
public function userMayCreateBookingForSlot(\User $user = null): bool
{
$user = $user ?? User::findCurrent();
return ConsultationBooking::userMayCreateBookingForRange($this->block->range, $user)
&& !$this->isOccupied()
&& !$this->isLocked();
}
/** /**
* @return string A string representation of the consultation slot. * @return string A string representation of the consultation slot.
......
...@@ -23,10 +23,13 @@ ...@@ -23,10 +23,13 @@
} }
} }
.consultation-free { .consultation-free {
color: $green; color: var(--green);
} }
.consultation-occupied { .consultation-occupied {
color: $red; color: var(--red);
}
.consultation-slot-not-bookable {
color: var(--light-gray-color);
} }
.consultation-overview { .consultation-overview {
......
...@@ -41,19 +41,23 @@ trait ConsultationHelper ...@@ -41,19 +41,23 @@ trait ConsultationHelper
return User::find($credentials['id']); return User::find($credentials['id']);
} }
protected function createBlockWithSlotsForRange(Range $range): ConsultationBlock protected function createBlockWithSlotsForRange(Range $range, array $additional_data = []): ConsultationBlock
{ {
$hour = date('H');
$begin = strtotime("today {$hour}:00:00");
$end = strtotime('+2 hours', $begin);
$blocks = ConsultationBlock::generateBlocks( $blocks = ConsultationBlock::generateBlocks(
$range, $range,
strtotime('today 8:00'), $begin,
strtotime('today 10:00'), $end,
date('w'), date('w'),
1 1
); );
$blocks = iterator_to_array($blocks); $blocks = iterator_to_array($blocks);
$block = reset($blocks); $block = reset($blocks);
$block->setData(self::$BLOCK_DATA); $block->setData(array_merge(self::$BLOCK_DATA, $additional_data));
$block->slots->exchangeArray($block->createSlots(15)); $block->slots->exchangeArray($block->createSlots(15));
foreach ($block->slots as $slot) { foreach ($block->slots as $slot) {
......
...@@ -6,6 +6,7 @@ use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse; ...@@ -6,6 +6,7 @@ use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse;
require_once __DIR__ . '/ConsultationHelper.php'; require_once __DIR__ . '/ConsultationHelper.php';
// TODO: Test locked blocks
class ConsultationsBookingCreateBySlotIndexTest extends Codeception\Test\Unit class ConsultationsBookingCreateBySlotIndexTest extends Codeception\Test\Unit
{ {
use ConsultationHelper; use ConsultationHelper;
...@@ -26,6 +27,24 @@ class ConsultationsBookingCreateBySlotIndexTest extends Codeception\Test\Unit ...@@ -26,6 +27,24 @@ class ConsultationsBookingCreateBySlotIndexTest extends Codeception\Test\Unit
); );
} }
public function testAutorMayCreateNotCreateBookingDueToLock(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$range = User::find($credentials['id']);
$block = $this->createBlockWithSlotsForRange($range, ['lock_time' => 2]);
$slot = $this->getSlotFromBlock($block);
$response = $this->createBooking(
$credentials,
$slot,
$this->tester->getCredentialsForTestAutor()['id'],
null
);
$this->assertEquals(409, $response->getStatusCode());
}
public function testSlotIsOccupied(): void public function testSlotIsOccupied(): void
{ {
$credentials = $this->tester->getCredentialsForTestDozent(); $credentials = $this->tester->getCredentialsForTestDozent();
......
...@@ -7,6 +7,7 @@ use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse; ...@@ -7,6 +7,7 @@ use WoohooLabs\Yang\JsonApi\Response\JsonApiResponse;
require_once __DIR__ . '/ConsultationHelper.php'; require_once __DIR__ . '/ConsultationHelper.php';
// TODO: Test locked blocks
class ConsultationsBookingCreateTest extends Codeception\Test\Unit class ConsultationsBookingCreateTest extends Codeception\Test\Unit
{ {
use ConsultationHelper; use ConsultationHelper;
...@@ -27,6 +28,24 @@ class ConsultationsBookingCreateTest extends Codeception\Test\Unit ...@@ -27,6 +28,24 @@ class ConsultationsBookingCreateTest extends Codeception\Test\Unit
); );
} }
public function testAutorMayCreateNotCreateBookingDueToLock(): void
{
$credentials = $this->tester->getCredentialsForTestDozent();
$range = User::find($credentials['id']);
$block = $this->createBlockWithSlotsForRange($range, ['lock_time' => 2]);
$slot = $this->getSlotFromBlock($block);
$response = $this->createBooking(
$credentials,
$slot,
$this->tester->getCredentialsForTestAutor()['id'],
null
);
$this->assertEquals(409, $response->getStatusCode());
}
public function testSlotIsOccupied(): void public function testSlotIsOccupied(): void
{ {
$credentials = $this->tester->getCredentialsForTestDozent(); $credentials = $this->tester->getCredentialsForTestDozent();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment