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

Add better tasks.

parent 081369a5
No related branches found
No related tags found
No related merge requests found
Showing
with 577 additions and 561 deletions
...@@ -70,11 +70,19 @@ class Course_CoursewareController extends CoursewareController ...@@ -70,11 +70,19 @@ 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
);
switch ($route) {
default:
Navigation::activateItem('course/courseware/tasks'); Navigation::activateItem('course/courseware/tasks');
PageLayout::setTitle(_('Courseware: Aufgaben'));
break;
}
$this->setTasksSidebar(); $this->setTasksSidebar();
} }
......
<?php
class AddDatesToCwTaskGroups extends Migration
{
public function description()
{
return 'TODO';
}
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`'
);
}
}
...@@ -489,6 +489,8 @@ class RouteMap ...@@ -489,6 +489,8 @@ 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);
$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, StructuralElementComment $resource) public static function canDeleteStructuralElementFeedback(User $user, StructuralElementComment $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)
...@@ -513,7 +533,6 @@ class Authority ...@@ -513,7 +533,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;
......
...@@ -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.end-date')) {
return 'Missing `review-start` attribute.';
}
$endDate = self::arrayGet($json, 'data.attributes.end-date');
if (!self::isValidTimestamp($endDate)) {
return '`end-date` is not an ISO 8601 timestamp.';
}
}
private function update(TaskGroup $taskGroup, array $json): TaskGroup
{
$endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date'));
$taskGroup->end_date = $endDate->getTimestamp();
$taskGroup->store();
return $taskGroup;
}
}
...@@ -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,7 @@ ...@@ -2,6 +2,7 @@
namespace Courseware; namespace Courseware;
use Statusgruppen;
use User; use User;
/** /**
...@@ -24,6 +25,10 @@ use User; ...@@ -24,6 +25,10 @@ use User;
* @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 +46,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -41,6 +46,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 +62,22 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -52,6 +62,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');
...@@ -59,21 +85,38 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject ...@@ -59,21 +85,38 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
return $solvers; return $solvers;
} }
public function getSubmitters(): iterable
{
return \DBManager::get()->fetchAll(
'SELECT solver_id, solver_type FROM cw_tasks WHERE task_group_id = ? AND submitted = 1',
[$this->getId()],
function ($row) {
switch ($row['solver_type']) {
case 'autor':
return \User::find($row['solver_id']);
case 'group':
return \Statusgruppen::find($row['solver_id']);
}
}
);
}
/** /**
* Export available data of a given user into a storage object * @param User|Statusgruppen $solver
* (an instance of the StoredUserData class) for that user.
* *
* @param StoredUserData $storage object to store data into * @return Task|null
*/ */
public static function exportUserData(\StoredUserData $storage) public function findTaskBySolver($solver)
{ {
$task_groups = \DBManager::get()->fetchAll( $row = \DBManager::get()->fetchOne(
'SELECT * FROM cw_task_groups WHERE lecturer_id = ?', 'SELECT id FROM cw_tasks WHERE task_group_id = ? AND solver_id = ? AND solver_type = ?',
[$storage->user_id] [
$this->getId(),
$solver->getId(),
$solver instanceof User ? 'autor' : 'group',
]
); );
if ($task_groups) {
$storage->addTabularData(_('Courseware Aufgaben'), 'cw_task_groups', $task_groups);
}
return empty($row) ? null : Task::find($row['id']);
} }
} }
...@@ -728,4 +728,17 @@ class Statusgruppen extends SimpleORMap implements PrivacyObject ...@@ -728,4 +728,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 { translate } from 'vue-gettext';
import * as defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json';
import eventBus from './event-bus';
interface StringDict {
[key: string]: string;
}
interface InstalledLanguage {
name: string;
selected: boolean;
}
interface InstalledLanguages {
[key: string]: InstalledLanguage;
}
type TranslationDict = StringDict;
interface TranslationDicts {
[key: string]: TranslationDict | null;
}
const DEFAULT_LANG = 'de_DE';
const DEFAULT_LANG_NAME = 'Deutsch';
const state = getInitialState();
const $gettext = translate.gettext.bind(translate);
const $ngettext = translate.ngettext.bind(translate);
const $gettextInterpolate = translate.gettextInterpolate.bind(translate);
export { $gettext, $ngettext, $gettextInterpolate, translate, getLocale, setLocale, getVueConfig };
function getLocale() {
return state.locale;
}
async function setLocale(locale = getInitialLocale()) {
if (!(locale in getInstalledLanguages())) {
throw new Error('Invalid locale: ' + locale);
}
state.locale = locale;
if (state.translations[state.locale] === null) {
const translations: TranslationDict = await getTranslations(state.locale);
state.translations[state.locale] = translations;
}
translate.initTranslations(state.translations, {
getTextPluginMuteLanguages: [DEFAULT_LANG],
getTextPluginSilent: false,
language: state.locale,
silent: false,
});
eventBus.emit('studip:set-locale', state.locale);
}
function getVueConfig() {
const availableLanguages = Object.entries(getInstalledLanguages()).reduce((memo, [lang, { name }]) => {
memo[lang] = name;
return memo;
}, {} as StringDict);
return {
availableLanguages,
defaultLanguage: DEFAULT_LANG,
muteLanguages: [DEFAULT_LANG],
silent: false,
translations: state.translations,
};
}
function getInitialState() {
const translations: TranslationDicts = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null;
return memo;
}, {} as TranslationDicts);
return {
locale: DEFAULT_LANG,
translations,
};
}
function getInitialLocale() {
for (const [lang, { selected }] of Object.entries(getInstalledLanguages())) {
if (selected) {
return lang;
}
}
return DEFAULT_LANG;
}
function getInstalledLanguages(): InstalledLanguages {
return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } };
}
async function getTranslations(locale: string): Promise<TranslationDict> {
try {
const language = locale.split(/[_-]/)[0];
const translation = await import(`../../../../locale/${language}/LC_MESSAGES/js-resources.json`);
return translation;
} catch (exception) {
console.error('Could not load locale: "' + locale + '"', exception);
return {};
}
}
declare module "vue-gettext" {
import GettextPlugin from 'vue-gettext';
declare namespace translate {
function getTranslation(msgid: any, n?: number, context?: any, defaultPlural?: any, language?: string): any;
function gettext(msgid: any, language?: string): any;
function pgettext(context: any, msgid: any, language?: string): any;
function ngettext(msgid: any, plural: any, n: any, language?: string): any;
function npgettext(context: any, msgid: any, plural: any, n: any, language?: string): any;
function initTranslations(translations: any, config: any): void;
const gettextInterpolate: any;
}
export { translate };
export default GettextPlugin;
}
<template>
<time :datetime="date.toISOString()">{{ formatted }}</time>
</template>
<script>
function formatDate(date) {
return pad(date.getDate()) + '.' + pad(date.getMonth() + 1) + '.' + date.getFullYear();
}
function pad(what) {
return what.toString().padStart(2, '0');
}
export default {
props: {
date: {
type: Date,
required: true,
},
},
computed: {
formatted() {
return formatDate(this.date);
},
},
};
</script>
This diff is collapsed.
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
<div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }"> <div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }">
<a href="#" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen"> <a href="#" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
<header :class="{ 'cw-collapsible-open': isOpen }" class="cw-collapsible-title"> <header :class="{ 'cw-collapsible-open': isOpen }" class="cw-collapsible-title">
<studip-icon v-if="icon" :shape="icon" /> {{ title }} <studip-icon v-if="icon" :shape="icon" />
<slot name="title" :is-open="isOpen">{{ title }}</slot>
</header> </header>
</a> </a>
<div class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }"> <div class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }">
<slot></slot> <slot :isOpen="isOpen"></slot>
</div> </div>
</div> </div>
</template> </template>
......
<template>
<studip-dialog
:title="$gettext('Feedback zur Aufgabe geben')"
:confirmText="$gettext('Speichern')"
confirmClass="accept"
:closeText="$gettext('Schließen')"
closeClass="cancel"
height="420"
@close="$emit('close')"
@confirm="create"
>
<template #dialogContent>
<form class="default" @submit.prevent="">
<label>
{{ $gettext('Feedback') }}
<textarea v-model="localContent" />
</label>
</form>
</template>
</studip-dialog>
</template>
<script>
export default {
props: ['content'],
data: () => ({
localContent: '',
}),
methods: {
resetLocalVars() {
this.localContent = this.content;
},
create() {
this.$emit('create', { content: this.localContent });
},
},
mounted() {
this.resetLocalVars();
},
watch: {
content(newValue) {
if (newValue !== this.localContent) {
this.resetLocalVars();
}
},
},
};
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment