Skip to content
Snippets Groups Projects
Commit 1728bc51 authored by Marcus Eibrink-Lunzenauer's avatar Marcus Eibrink-Lunzenauer
Browse files

Courseware Aufgaben erweitern (StEP3286)

Merge request !2445
parent 9750421a
Branches
No related tags found
No related merge requests found
Showing
with 607 additions and 82 deletions
...@@ -79,11 +79,16 @@ class Course_CoursewareController extends CoursewareController ...@@ -79,11 +79,16 @@ class Course_CoursewareController extends CoursewareController
} }
} }
public function tasks_action(): void public function tasks_action($route = null): void
{ {
global $perm, $user; $this->is_teacher = $GLOBALS['perm']->have_studip_perm(
$this->is_teacher = $perm->have_studip_perm('tutor', Context::getId(), $user->id); 'tutor',
Context::getId(),
$GLOBALS['user']->id
);
Navigation::activateItem('course/courseware/tasks'); Navigation::activateItem('course/courseware/tasks');
PageLayout::setTitle(_('Courseware: Aufgaben'));
$this->setTasksSidebar(); $this->setTasksSidebar();
} }
......
<?php
class AddDatesToCwTaskGroups extends Migration
{
public function description()
{
return 'Add start_date and end_date to table cw_task_groups.';
}
public function up()
{
$dbm = \DBManager::get();
$dbm->exec(
"ALTER TABLE `cw_task_groups`
ADD `start_date` INT NOT NULL AFTER `title`,
ADD `end_date` INT NOT NULL AFTER `start_date`"
);
$dbm->exec('UPDATE `cw_task_groups` SET `start_date`=`mkdate`');
$dbm->exec(
'UPDATE `cw_task_groups` AS tg SET tg.`end_date` = ( SELECT MAX(t.`submission_date`) FROM `cw_tasks` t WHERE t.`task_group_id` = tg.`id` )'
);
$dbm->exec('ALTER TABLE `cw_tasks` DROP `submission_date`');
}
public function down()
{
$dbm = \DBManager::get();
$dbm->exec("ALTER TABLE `cw_tasks` ADD `submission_date` int(11) NOT NULL AFTER `solver_type`");
$dbm->exec('UPDATE `cw_tasks` AS t INNER JOIN cw_task_groups tg ON t.`task_group_id` = tg.`id` SET t.`submission_date` = tg.`end_date`');
$dbm->exec(
'ALTER TABLE `cw_task_groups`
DROP `start_date`,
DROP `end_date`'
);
}
}
...@@ -501,6 +501,13 @@ class RouteMap ...@@ -501,6 +501,13 @@ class RouteMap
$group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::class); $group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::class);
$group->post('/courseware-task-groups', Routes\Courseware\TaskGroupsCreate::class); $group->post('/courseware-task-groups', Routes\Courseware\TaskGroupsCreate::class);
$group->patch('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsUpdate::class);
$group->delete('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsDelete::class);
$this->addRelationship(
$group,
'/courseware-task-groups/{id}/relationships/solvers',
Routes\Courseware\Rel\SolversOfTaskGroup::class
);
$group->get('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackShow::class); $group->get('/courseware-task-feedback/{id}', Routes\Courseware\TaskFeedbackShow::class);
$group->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class); $group->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class);
......
...@@ -23,7 +23,13 @@ use User; ...@@ -23,7 +23,13 @@ use User;
use Course; use Course;
/** /**
* @SuppressWarnings(PHPMD.CamelCaseParameterName)
* @SuppressWarnings(PHPMD.CamelCaseVariableName)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.ExcessivePublicCount)
* @SuppressWarnings(PHPMD.StaticAccess)
* @SuppressWarnings(PHPMD.Superglobals)
* @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.TooManyMethods)
* @SuppressWarnings(PHPMD.TooManyPublicMethods) * @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/ */
...@@ -306,6 +312,16 @@ class Authority ...@@ -306,6 +312,16 @@ class Authority
return $resource['lecturer_id'] === $user->id; return $resource['lecturer_id'] === $user->id;
} }
public static function canUpdateTaskGroup(User $user, TaskGroup $resource): bool
{
return self::canCreateTasks($user, $resource->target);
}
public static function canDeleteTaskGroup(User $user, TaskGroup $resource): bool
{
return self::canUpdateTaskGroup($user, $resource);
}
public static function canShowTask(User $user, Task $resource): bool public static function canShowTask(User $user, Task $resource): bool
{ {
return self::canUpdateTask($user, $resource); return self::canUpdateTask($user, $resource);
...@@ -332,6 +348,11 @@ class Authority ...@@ -332,6 +348,11 @@ class Authority
return self::canCreateTasks($user, $resource->structural_element) && !$resource->userIsASolver($user); return self::canCreateTasks($user, $resource->structural_element) && !$resource->userIsASolver($user);
} }
public static function canRenewTask(User $user, Task $resource): bool
{
return self::canDeleteTask($user, $resource);
}
public static function canCreateTaskFeedback(User $user, Task $resource): bool public static function canCreateTaskFeedback(User $user, Task $resource): bool
{ {
return self::canCreateTasks($user, $resource->structural_element); return self::canCreateTasks($user, $resource->structural_element);
...@@ -352,7 +373,6 @@ class Authority ...@@ -352,7 +373,6 @@ class Authority
return self::canCreateTaskFeedback($user, $resource); return self::canCreateTaskFeedback($user, $resource);
} }
public static function canIndexStructuralElementComments(User $user, StructuralElement $resource) public static function canIndexStructuralElementComments(User $user, StructuralElement $resource)
{ {
return self::canShowStructuralElement($user, $resource); return self::canShowStructuralElement($user, $resource);
...@@ -407,7 +427,8 @@ class Authority ...@@ -407,7 +427,8 @@ class Authority
public static function canShowStructuralElementFeedback(User $user, StructuralElementFeedback $resource) public static function canShowStructuralElementFeedback(User $user, StructuralElementFeedback $resource)
{ {
return $resource->user_id === $user->id || self::canUpdateStructuralElement($user, $resource->structural_element); return $resource->user_id === $user->id ||
self::canUpdateStructuralElement($user, $resource->structural_element);
} }
public static function canDeleteStructuralElementFeedback(User $user, StructuralElementFeedback $resource) public static function canDeleteStructuralElementFeedback(User $user, StructuralElementFeedback $resource)
...@@ -415,7 +436,6 @@ class Authority ...@@ -415,7 +436,6 @@ class Authority
return self::canUpdateStructuralElementFeedback($user, $resource); return self::canUpdateStructuralElementFeedback($user, $resource);
} }
public static function canShowTemplate(User $user, Template $resource) public static function canShowTemplate(User $user, Template $resource)
{ {
// templates are for everybody, aren't they? // templates are for everybody, aren't they?
...@@ -430,7 +450,7 @@ class Authority ...@@ -430,7 +450,7 @@ class Authority
public static function canCreateTemplate(User $user) public static function canCreateTemplate(User $user)
{ {
return $GLOBALS['perm']->have_perm('admin'); return $GLOBALS['perm']->have_perm('admin', $user->id);
} }
public static function canUpdateTemplate(User $user, Template $resource) public static function canUpdateTemplate(User $user, Template $resource)
...@@ -518,7 +538,6 @@ class Authority ...@@ -518,7 +538,6 @@ class Authority
return $request_user->id === $user->id; return $request_user->id === $user->id;
} }
public static function canShowClipboard(User $user, Clipboard $resource): bool public static function canShowClipboard(User $user, Clipboard $resource): bool
{ {
return $resource->user_id === $user->id; return $resource->user_id === $user->id;
......
<?php
namespace JsonApi\Routes\Courseware\Rel;
use Courseware\StructuralElement;
use Courseware\Task;
use Courseware\TaskGroup;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\Errors\UnprocessableEntityException;
use JsonApi\Routes\Courseware\Authority;
use JsonApi\Routes\RelationshipsController;
use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
use JsonApi\Schemas\StatusGroup as StatusGroupSchema;
use JsonApi\Schemas\User as UserSchema;
use Psr\Http\Message\ServerRequestInterface as Request;
use Statusgruppen;
use User;
/**
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class SolversOfTaskGroup extends RelationshipsController
{
protected $allowedPagingParameters = ['offset', 'limit'];
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function fetchRelationship(Request $request, $related)
{
$solvers = $related->getSolvers();
$total = count($solvers);
return $this->getPaginatedIdentifiersResponse(array_slice($solvers, ...$this->getOffsetAndLimit()), $total);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function addToRelationship(Request $request, $related)
{
$this->createTaskFor(
$related,
array_filter($this->validateSolvers($related, $this->validate($request)), function ($solver) use (
$related
) {
return !$related->findTaskBySolver($solver);
})
);
return $this->getCodeResponse(204);
}
protected function findRelated(array $args)
{
$related = TaskGroup::find($args['id']);
if (!$related) {
throw new RecordNotFoundException();
}
return $related;
}
protected function authorize(Request $request, $resource)
{
switch ($request->getMethod()) {
case 'GET':
return Authority::canShowTaskGroup($this->getUser($request), $resource);
case 'POST':
return Authority::canUpdateTaskGroup($this->getUser($request), $resource);
default:
return false;
}
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function getRelationshipSelfLink($resource, $schema, $userData)
{
return $schema->getRelationshipSelfLink($resource, TaskGroupSchema::REL_SOLVERS);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function getRelationshipRelatedLink($resource, $schema, $userData)
{
return $schema->getRelationshipRelatedLink($resource, TaskGroupSchema::REL_SOLVERS);
}
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data')) {
return 'Missing `data` member at document´s top level.';
}
$data = self::arrayGet($json, 'data');
if (!is_array($data)) {
return 'Document´s `data` must be an array.';
}
foreach ($data as $item) {
if (!in_array(self::arrayGet($item, 'type'), [UserSchema::TYPE, StatusGroupSchema::TYPE])) {
return 'Wrong `type` in document´s `data`.';
}
if (!self::arrayGet($item, 'id')) {
return 'Missing `id` of document´s `data`.';
}
}
}
private function validateSolvers(TaskGroup $taskGroup, iterable $json): iterable
{
if (!$taskGroup->course) {
return [];
}
$solvers = [];
foreach ($json['data'] as $item) {
$solver = $this->findSolver($item);
if (!$solver) {
throw new RecordNotFoundException();
}
if (!$this->validateSolver($taskGroup, $solver)) {
throw new UnprocessableEntityException();
}
$solvers[] = $solver;
}
return $solvers;
}
/**
* @return Statusgruppen|User|null
*/
private function findSolver($json)
{
switch ($json['type']) {
case 'status-groups':
return Statusgruppen::find($json['id']);
case 'users':
return User::find($json['id']);
}
return null;
}
/**
* @param Statusgruppen|User $solver
*
* @SuppressWarnings(PHPMD.Superglobals)
*/
private function validateSolver(TaskGroup $taskGroup, $solver): bool
{
if ($solver instanceof User) {
return $GLOBALS['perm']->have_studip_perm('autor', $taskGroup->course->id, $solver->id);
}
if ($solver instanceof Statusgruppen) {
return $taskGroup->course->id === $solver->range_id;
}
return false;
}
/**
* @param array<User|Statusgruppen> $solvers
*/
private function createTaskFor(TaskGroup $taskGroup, $solvers): void
{
$template = $this->getTaskTemplate($taskGroup);
if (!$template) {
throw new RuntimeException();
}
foreach ($solvers as $solver) {
$task = Task::build([
'task_group_id' => $taskGroup->id,
'solver_id' => $solver->id,
'solver_type' => $this->getSolverType($solver),
]);
$taskElement = $template->copy($taskGroup->lecturer, $taskGroup->target, 'task');
$taskElement->title = $taskGroup->title;
$taskElement->store();
$task['structural_element_id'] = $taskElement->id;
$task->store();
}
}
private function getTaskTemplate(TaskGroup $taskGroup): StructuralElement
{
return StructuralElement::find($taskGroup->task_template_id);
}
/**
* @param User|Statusgruppen $solver
*/
private function getSolverType($solver): string
{
$solverTypes = [\User::class => 'autor', \Statusgruppen::class => 'group'];
return $solverTypes[get_class($solver)];
}
}
...@@ -65,14 +65,20 @@ class TaskGroupsCreate extends JsonApiController ...@@ -65,14 +65,20 @@ class TaskGroupsCreate extends JsonApiController
if (!self::arrayHas($json, 'data.attributes.title')) { if (!self::arrayHas($json, 'data.attributes.title')) {
return 'Missing `title` attribute.'; return 'Missing `title` attribute.';
} }
if (!self::arrayHas($json, 'data.attributes.submission-date')) { if (!self::arrayHas($json, 'data.attributes.start-date')) {
return 'Missing `submission-date` attribute.'; return 'Missing `start-date` attribute.';
} }
$submissionDate = self::arrayGet($json, 'data.attributes.submission-date'); $startDate = self::arrayGet($json, 'data.attributes.start-date');
if (!self::isValidTimestamp($submissionDate)) { if (!self::isValidTimestamp($startDate)) {
return '`submission-date` is not an ISO 8601 timestamp.'; return '`start-date` is not an ISO 8601 timestamp.';
}
if (!self::arrayHas($json, 'data.attributes.end-date')) {
return 'Missing `end-date` attribute.';
}
$endDate = self::arrayGet($json, 'data.attributes.end-date');
if (!self::isValidTimestamp($endDate)) {
return '`end-date` is not an ISO 8601 timestamp.';
} }
if (!self::arrayHas($json, 'data.relationships.target')) { if (!self::arrayHas($json, 'data.relationships.target')) {
return 'Missing `target` relationship.'; return 'Missing `target` relationship.';
} }
...@@ -165,8 +171,8 @@ class TaskGroupsCreate extends JsonApiController ...@@ -165,8 +171,8 @@ class TaskGroupsCreate extends JsonApiController
$target = $this->getTargetFromJson($json); $target = $this->getTargetFromJson($json);
$solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', ''); $solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', '');
$submissionDate = self::arrayGet($json, 'data.attributes.submission-date', ''); $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.start-date', ''));
$submissionDate = self::fromISO8601($submissionDate); $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date', ''));
$title = self::arrayGet($json, 'data.attributes.title', ''); $title = self::arrayGet($json, 'data.attributes.title', '');
/** @var TaskGroup $taskGroup */ /** @var TaskGroup $taskGroup */
...@@ -177,6 +183,8 @@ class TaskGroupsCreate extends JsonApiController ...@@ -177,6 +183,8 @@ class TaskGroupsCreate extends JsonApiController
'task_template_id' => $taskTemplate->getId(), 'task_template_id' => $taskTemplate->getId(),
'solver_may_add_blocks' => $solverMayAddBlocks, 'solver_may_add_blocks' => $solverMayAddBlocks,
'title' => $title, 'title' => $title,
'start_date' => $startDate->getTimestamp(),
'end_date' => $endDate->getTimestamp(),
]); ]);
foreach ($solvers as $solver) { foreach ($solvers as $solver) {
...@@ -184,7 +192,6 @@ class TaskGroupsCreate extends JsonApiController ...@@ -184,7 +192,6 @@ class TaskGroupsCreate extends JsonApiController
'task_group_id' => $taskGroup->getId(), 'task_group_id' => $taskGroup->getId(),
'solver_id' => $solver->getId(), 'solver_id' => $solver->getId(),
'solver_type' => $this->getSolverType($solver), 'solver_type' => $this->getSolverType($solver),
'submission_date' => $submissionDate->getTimestamp(),
]); ]);
// copy task template // copy task template
......
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\TaskGroup;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Delete one TaskGroup.
*/
class TaskGroupsDelete extends JsonApiController
{
/**
* @param array $args
* @return Response
*
* @SuppressWarnings(PHPMD.StaticAccess)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function __invoke(Request $request, Response $response, $args)
{
/** @var ?TaskGroup $resource */
$resource = TaskGroup::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
if (!Authority::canDeleteTaskGroup($this->getUser($request), $resource)) {
throw new AuthorizationFailedException();
}
$resource->delete();
return $this->getCodeResponse(204);
}
}
<?php
namespace JsonApi\Routes\Courseware;
use Courseware\TaskGroup;
use JsonApi\Errors\AuthorizationFailedException;
use JsonApi\Errors\RecordNotFoundException;
use JsonApi\JsonApiController;
use JsonApi\Routes\TimestampTrait;
use JsonApi\Routes\ValidationTrait;
use JsonApi\Schemas\Courseware\TaskGroup as TaskGroupSchema;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use User;
/**
* Updates one TaskGroup.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class TaskGroupsUpdate extends JsonApiController
{
use TimestampTrait;
use ValidationTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @param array $args
* @return Response
*/
public function __invoke(Request $request, Response $response, $args)
{
/** @var ?\Courseware\TaskGroup $resource */
$resource = TaskGroup::find($args['id']);
if (!$resource) {
throw new RecordNotFoundException();
}
$json = $this->validate($request, $resource);
$user = $this->getUser($request);
if (!Authority::canUpdateTaskGroup($user, $resource)) {
throw new AuthorizationFailedException();
}
$process = $this->update($resource, $json);
return $this->getContentResponse($process);
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameters)
*
* @param array $json
* @param mixed $data
*
* @return string|void
*/
protected function validateResourceDocument($json, $data)
{
if (!self::arrayHas($json, 'data')) {
return 'Missing `data` member at document´s top level.';
}
if (TaskGroupSchema::TYPE !== self::arrayGet($json, 'data.type')) {
return 'Invalid `type` of document´s `data`.';
}
if (!self::arrayHas($json, 'data.attributes.start-date')) {
return 'Missing `start-date` attribute.';
}
$startDate = self::arrayGet($json, 'data.attributes.start-date');
if (!self::isValidTimestamp($startDate)) {
return '`start-date` is not an ISO 8601 timestamp.';
}
if (!self::arrayHas($json, 'data.attributes.end-date')) {
return 'Missing `end-date` attribute.';
}
$endDate = self::arrayGet($json, 'data.attributes.end-date');
if (!self::isValidTimestamp($endDate)) {
return '`end-date` is not an ISO 8601 timestamp.';
}
if (self::fromISO8601($startDate) > self::fromISO8601($endDate)) {
return '`start-date` is later than `end-date`';
}
}
private function update(TaskGroup $taskGroup, array $json): TaskGroup
{
$startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.start-date'));
$endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date'));
$taskGroup->start_date = $startDate->getTimestamp();
$taskGroup->end_date = $endDate->getTimestamp();
$taskGroup->store();
return $taskGroup;
}
}
...@@ -77,9 +77,10 @@ class TasksIndex extends JsonApiController ...@@ -77,9 +77,10 @@ class TasksIndex extends JsonApiController
} }
} }
private function findTasksByCourse(\Course $course): \SimpleCollection private function findTasksByCourse(\Course $course, bool $showNotYetActive = true): \SimpleCollection
{ {
$taskGroups = TaskGroup::findBySQL('seminar_id = ?', [$course->getId()]); $whereClause = $showNotYetActive ? 'seminar_id = ?' : 'start_date <= UNIX_TIMESTAMP() AND seminar_id = ?';
$taskGroups = TaskGroup::findBySQL($whereClause, [$course->getId()]);
$tasks = []; $tasks = [];
foreach ($taskGroups as $taskGroup) { foreach ($taskGroups as $taskGroup) {
...@@ -98,7 +99,7 @@ class TasksIndex extends JsonApiController ...@@ -98,7 +99,7 @@ class TasksIndex extends JsonApiController
}) })
->pluck('id'); ->pluck('id');
return $this->findTasksByCourse($course)->filter(function ($task) use ($user, $groupIds) { return $this->findTasksByCourse($course, false)->filter(function ($task) use ($user, $groupIds) {
return ('autor' === $task['solver_type'] && $task['solver_id'] === $user->getId()) || return ('autor' === $task['solver_type'] && $task['solver_id'] === $user->getId()) ||
('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds)); ('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds));
}); });
......
...@@ -13,6 +13,8 @@ use Psr\Http\Message\ServerRequestInterface as Request; ...@@ -13,6 +13,8 @@ use Psr\Http\Message\ServerRequestInterface as Request;
/** /**
* Update one Task. * Update one Task.
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/ */
class TasksUpdate extends JsonApiController class TasksUpdate extends JsonApiController
{ {
...@@ -32,7 +34,8 @@ class TasksUpdate extends JsonApiController ...@@ -32,7 +34,8 @@ class TasksUpdate extends JsonApiController
throw new RecordNotFoundException(); throw new RecordNotFoundException();
} }
$json = $this->validate($request, $resource); $json = $this->validate($request, $resource);
if (!Authority::canUpdateTask($user = $this->getUser($request), $resource)) { $user = $this->getUser($request);
if (!Authority::canUpdateTask($user, $resource)) {
throw new AuthorizationFailedException(); throw new AuthorizationFailedException();
} }
$resource = $this->updateTask($user, $resource, $json); $resource = $this->updateTask($user, $resource, $json);
...@@ -66,53 +69,35 @@ class TasksUpdate extends JsonApiController ...@@ -66,53 +69,35 @@ class TasksUpdate extends JsonApiController
private function updateTask(\User $user, Task $resource, array $json): Task private function updateTask(\User $user, Task $resource, array $json): Task
{ {
if (Authority::canDeleteTask($user, $resource)) { if (Authority::canRenewTask($user, $resource)) {
if (self::arrayHas($json, 'data.attributes.renewal')) { return $this->renewTask($resource, $json);
$newRenewalState = self::arrayGet($json, 'data.attributes.renewal');
if ('declined' === $newRenewalState) {
$resource->renewal = $newRenewalState;
} }
if ('granted' === $newRenewalState && self::arrayHas($json, 'data.attributes.renewal-date')) {
$renewalDate = self::arrayGet($json, 'data.attributes.renewal-date', '');
$renewalDate = self::fromISO8601($renewalDate);
$resource->renewal = $newRenewalState; if (self::arrayGet($json, 'data.attributes.submitted') === true && $resource->canSubmit()) {
$resource->renewal_date = $renewalDate->getTimestamp(); $resource->submitTask();
}
}
} else {
if (self::arrayHas($json, 'data.attributes.submitted')) {
$newSubmittedState = self::arrayGet($json, 'data.attributes.submitted');
if ($this->canSubmit($resource, $newSubmittedState)) {
$resource->submitted = $newSubmittedState;
if ('pending' === $resource->renewal) {
$resource->renewal = '';
}
}
}
if (self::arrayHas($json, 'data.attributes.renewal')) {
$newRenewalState = self::arrayGet($json, 'data.attributes.renewal');
if ('pending' === $newRenewalState) {
$resource->renewal = $newRenewalState;
}
}
} }
$resource->store(); if (self::arrayGet($json, 'data.attributes.renewal') === 'pending') {
$resource->requestRenewal();
}
return $resource; return $resource;
} }
private function canSubmit(Task $resource, string $newSubmittedState): bool private function renewTask(Task $resource, array $json): Task
{ {
$now = time(); switch (self::arrayGet($json, 'data.attributes.renewal')) {
if (1 === (int) $resource->submitted || !$newSubmittedState) { case 'declined':
return false; $resource->declineRenewalRequest();
} break;
if ('granted' === $resource->renewal) {
return $now <= $resource->renewal_date; case 'granted':
} else { $resource->grantRenewalRequest(
return $now <= $resource->submission_date; self::fromISO8601(self::arrayGet($json, 'data.attributes.renewal-date'))
);
break;
} }
return $resource;
} }
} }
...@@ -59,17 +59,17 @@ class SchemaMap ...@@ -59,17 +59,17 @@ class SchemaMap
\Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class, \Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class,
\Courseware\Container::class => Schemas\Courseware\Container::class, \Courseware\Container::class => Schemas\Courseware\Container::class,
\Courseware\Instance::class => Schemas\Courseware\Instance::class, \Courseware\Instance::class => Schemas\Courseware\Instance::class,
\Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class,
\Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class, \Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class,
\Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class, \Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::class,
\Courseware\StructuralElementFeedback::class => Schemas\Courseware\StructuralElementFeedback::class, \Courseware\StructuralElementFeedback::class => Schemas\Courseware\StructuralElementFeedback::class,
\Courseware\Unit::class => Schemas\Courseware\Unit::class,
\Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class,
\Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class,
\Courseware\Task::class => Schemas\Courseware\Task::class, \Courseware\Task::class => Schemas\Courseware\Task::class,
\Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class,
\Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class, \Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class,
\Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::class,
\Courseware\Template::class => Schemas\Courseware\Template::class, \Courseware\Template::class => Schemas\Courseware\Template::class,
\Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class, \Courseware\Unit::class => Schemas\Courseware\Unit::class,
\Courseware\UserDataField::class => Schemas\Courseware\UserDataField::class,
\Courseware\UserProgress::class => Schemas\Courseware\UserProgress::class,
]; ];
} }
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
namespace JsonApi\Schemas\Courseware; namespace JsonApi\Schemas\Courseware;
use Courseware\Task as TaskModel;
use JsonApi\Routes\Courseware\Authority as CoursewareAuthority;
use JsonApi\Schemas\SchemaProvider; use JsonApi\Schemas\SchemaProvider;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link; use Neomerx\JsonApi\Schema\Link;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace JsonApi\Schemas\Courseware; namespace JsonApi\Schemas\Courseware;
use Courseware\StructuralElement; use Courseware\StructuralElement;
use Courseware\TaskGroup as TaskGroupModel;
use JsonApi\Schemas\SchemaProvider; use JsonApi\Schemas\SchemaProvider;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface; use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Identifier; use Neomerx\JsonApi\Schema\Identifier;
...@@ -35,6 +36,8 @@ class TaskGroup extends SchemaProvider ...@@ -35,6 +36,8 @@ class TaskGroup extends SchemaProvider
return [ return [
'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'], 'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'],
'title' => (string) $resource->title, 'title' => (string) $resource->title,
'start-date' => date('c', $resource['start_date']),
'end-date' => date('c', $resource['end_date']),
'mkdate' => date('c', $resource['mkdate']), 'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate']), 'chdate' => date('c', $resource['chdate']),
]; ];
......
...@@ -31,7 +31,9 @@ use User; ...@@ -31,7 +31,9 @@ use User;
* @property \Statusgruppen $group belongs_to \Statusgruppen * @property \Statusgruppen $group belongs_to \Statusgruppen
* @property \Course $course belongs_to \Course * @property \Course $course belongs_to \Course
* @property TaskFeedback|null $task_feedback belongs_to TaskFeedback * @property TaskFeedback|null $task_feedback belongs_to TaskFeedback
* @property mixed $solver additional field * @property-read \User|\Statusgruppen|null $solver additional field
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/ */
class Task extends \SimpleORMap class Task extends \SimpleORMap
{ {
...@@ -80,6 +82,10 @@ class Task extends \SimpleORMap ...@@ -80,6 +82,10 @@ class Task extends \SimpleORMap
'get' => 'getSolver', 'get' => 'getSolver',
'set' => false, 'set' => false,
]; ];
$config['additional_fields']['submission_date'] = [
'get' => 'getSubmissionDate',
'set' => false,
];
parent::configure($config); parent::configure($config);
} }
...@@ -171,6 +177,11 @@ class Task extends \SimpleORMap ...@@ -171,6 +177,11 @@ class Task extends \SimpleORMap
return null; return null;
} }
public function getSubmissionDate(): int
{
return $this->task_group['end_date'];
}
public function getTaskProgress(): float public function getTaskProgress(): float
{ {
$children = $this->structural_element->findDescendants(); $children = $this->structural_element->findDescendants();
...@@ -185,6 +196,45 @@ class Task extends \SimpleORMap ...@@ -185,6 +196,45 @@ class Task extends \SimpleORMap
return $progress * 100; return $progress * 100;
} }
public function canSubmit(): bool
{
return !$this->submitted
&& time() <= ('granted' === $this->renewal ? $this->renewal_date : $this->submission_date);
}
public function submitTask(): void
{
$this->submitted = 1;
if ('pending' === $this->renewal) {
$this->renewal = '';
}
$this->store();
}
public function isRenewed(): bool
{
return $this->renewal === 'granted';
}
public function requestRenewal(): void
{
$this->renewal = 'pending';
$this->store();
}
public function declineRenewalRequest(): void
{
$this->renewal = 'declined';
$this->store();
}
public function grantRenewalRequest(\DateTime $renewalDate): void
{
$this->renewal = 'granted';
$this->renewal_date = $renewalDate->getTimestamp();
$this->store();
}
private function getStructuralElementProgress(StructuralElement $structural_element): float private function getStructuralElementProgress(StructuralElement $structural_element): float
{ {
$containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]); $containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]);
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
namespace Courseware; namespace Courseware;
use DBManager;
use Statusgruppen;
use User; use User;
/** /**
...@@ -19,11 +21,17 @@ use User; ...@@ -19,11 +21,17 @@ use User;
* @property int $task_template_id database column * @property int $task_template_id database column
* @property int $solver_may_add_blocks database column * @property int $solver_may_add_blocks database column
* @property string $title database column * @property string $title database column
* @property int $start_date database column
* @property int $end_date database column
* @property int $mkdate database column * @property int $mkdate database column
* @property int $chdate database column * @property int $chdate database column
* @property \SimpleORMapCollection|Task[] $tasks has_many Task * @property \SimpleORMapCollection|Task[] $tasks has_many Task
* @property \User $lecturer belongs_to \User * @property \User $lecturer belongs_to \User
* @property \Course $course belongs_to \Course * @property \Course $course belongs_to \Course
* @property \Courseware\StructuralElement $target belongs_to Courseware\StructuralElement
* @property \SimpleORMapCollection $tasks has_many Courseware\Task
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/ */
class TaskGroup extends \SimpleORMap implements \PrivacyObject class TaskGroup extends \SimpleORMap implements \PrivacyObject
{ {
...@@ -41,6 +49,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -41,6 +49,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
'foreign_key' => 'seminar_id', 'foreign_key' => 'seminar_id',
]; ];
$config['belongs_to']['target'] = [
'class_name' => StructuralElement::class,
'foreign_key' => 'target_id',
];
$config['has_many']['tasks'] = [ $config['has_many']['tasks'] = [
'class_name' => Task::class, 'class_name' => Task::class,
'assoc_foreign_key' => 'task_group_id', 'assoc_foreign_key' => 'task_group_id',
...@@ -52,6 +65,22 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -52,6 +65,22 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
parent::configure($config); parent::configure($config);
} }
/**
* Export available data of a given user into a storage object
* (an instance of the StoredUserData class) for that user.
*
* @param StoredUserData $storage object to store data into
*/
public static function exportUserData(\StoredUserData $storage)
{
$task_groups = DBManager::get()->fetchAll('SELECT * FROM cw_task_groups WHERE lecturer_id = ?', [
$storage->user_id,
]);
if ($task_groups) {
$storage->addTabularData(_('Courseware Aufgaben'), 'cw_task_groups', $task_groups);
}
}
public function getSolvers(): iterable public function getSolvers(): iterable
{ {
$solvers = $this->tasks->pluck('solver'); $solvers = $this->tasks->pluck('solver');
...@@ -60,20 +89,45 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -60,20 +89,45 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
} }
/** /**
* Export available data of a given user into a storage object * Returns all submitters of this TaskGroup.
* (an instance of the StoredUserData class) for that user.
* *
* @param StoredUserData $storage object to store data into * @returns iterable all the submitters of this TaskGroup.
*/ */
public static function exportUserData(\StoredUserData $storage) public function getSubmitters(): iterable
{ {
$task_groups = \DBManager::get()->fetchAll( return DBManager::get()->fetchAll(
'SELECT * FROM cw_task_groups WHERE lecturer_id = ?', 'SELECT solver_id, solver_type FROM cw_tasks WHERE task_group_id = ? AND submitted = 1',
[$storage->user_id] [$this->getId()],
function ($row) {
switch ($row['solver_type']) {
case 'autor':
return \User::find($row['solver_id']);
case 'group':
return \Statusgruppen::find($row['solver_id']);
}
}
); );
if ($task_groups) {
$storage->addTabularData(_('Courseware Aufgaben'), 'cw_task_groups', $task_groups);
} }
/**
* Returns the task of this TaskGroup given to $solver.
*
* @param User|Statusgruppen $solver
*
* @return Task|null
*/
public function findTaskBySolver($solver)
{
$row = DBManager::get()->fetchOne(
'SELECT id FROM cw_tasks WHERE task_group_id = ? AND solver_id = ? AND solver_type = ?',
[
$this->getId(),
$solver->getId(),
$solver instanceof User ? 'autor' : 'group',
]
);
return empty($row) ? null : Task::find($row['id']);
} }
} }
...@@ -740,4 +740,17 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject ...@@ -740,4 +740,17 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject
} }
} }
} }
/**
* Checks if a user is a member of a group.
*
* @param string $user_id The user id
* @return boolean <b>true</b> if user is a member of this group
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public static function isMemberOf(string $gruppenId, string $userId): bool
{
return StatusgruppeUser::countBySql('statusgruppe_id = ? AND user_id = ?', [$gruppenId, $userId]) !== 0;
}
} }
import { $gettext } from '../lib/gettext.js'; import { $gettext } from '../lib/gettext';
import eventBus from "../lib/event-bus.ts"; import eventBus from "../lib/event-bus.ts";
/* ------------------------------------------------------------------------ /* ------------------------------------------------------------------------
......
import { $gettext } from '../lib/gettext.js'; import { $gettext } from '../lib/gettext';
$(document).on('click', '.consultation-delete-check:not(.ignore)', event => { $(document).on('click', '.consultation-delete-check:not(.ignore)', event => {
const form = $(event.target).closest('form'); const form = $(event.target).closest('form');
......
import { $gettext } from '../lib/gettext.js'; import { $gettext } from '../lib/gettext';
$(document).on('click', 'a.copyable-link', function (event) { $(document).on('click', 'a.copyable-link', function (event) {
event.preventDefault(); event.preventDefault();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment