aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/course/courseware.php11
-rw-r--r--db/migrations/5.5.12_add_dates_to_cw_task_groups.php35
-rw-r--r--db/migrations/5.5.24_new_external_pages.php (renamed from db/migrations/5.5.12_new_external_pages.php)0
-rw-r--r--lib/classes/JsonApi/RouteMap.php7
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/Authority.php33
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php207
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php25
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php38
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php99
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TasksIndex.php7
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php65
-rw-r--r--lib/classes/JsonApi/SchemaMap.php10
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/Task.php2
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php3
-rw-r--r--lib/models/Courseware/Task.php52
-rw-r--r--lib/models/Courseware/TaskGroup.php76
-rw-r--r--lib/models/Statusgruppen.php13
-rw-r--r--resources/assets/javascripts/bootstrap/application.js2
-rw-r--r--resources/assets/javascripts/bootstrap/consultations.js2
-rw-r--r--resources/assets/javascripts/bootstrap/copyable_links.js2
-rw-r--r--resources/assets/javascripts/bootstrap/data_secure.js2
-rw-r--r--resources/assets/javascripts/bootstrap/forms.js2
-rw-r--r--resources/assets/javascripts/bootstrap/multi_select.js2
-rw-r--r--resources/assets/javascripts/bootstrap/mvv_difflog.js2
-rw-r--r--resources/assets/javascripts/bootstrap/raumzeit.js2
-rw-r--r--resources/assets/javascripts/bootstrap/resources.js2
-rw-r--r--resources/assets/javascripts/bootstrap/studip_helper_attributes.js2
-rw-r--r--resources/assets/javascripts/chunks/tablesorter.js2
-rw-r--r--resources/assets/javascripts/chunks/vue.js2
-rw-r--r--resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js2
-rw-r--r--resources/assets/javascripts/cke/studip-a11y-dialog/ui.js2
-rw-r--r--resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js2
-rw-r--r--resources/assets/javascripts/cke/wiki-link/formview.js2
-rw-r--r--resources/assets/javascripts/cke/wiki-link/ui.js2
-rw-r--r--resources/assets/javascripts/init.js2
-rw-r--r--resources/assets/javascripts/jquery-bundle.js4
-rw-r--r--resources/assets/javascripts/lib/admission.js2
-rw-r--r--resources/assets/javascripts/lib/big_image_handler.js2
-rw-r--r--resources/assets/javascripts/lib/calendar.js2
-rw-r--r--resources/assets/javascripts/lib/dialog.js2
-rw-r--r--resources/assets/javascripts/lib/files.js2
-rw-r--r--resources/assets/javascripts/lib/folders.js2
-rw-r--r--resources/assets/javascripts/lib/forum.js2
-rw-r--r--resources/assets/javascripts/lib/gettext.ts (renamed from resources/assets/javascripts/lib/gettext.js)35
-rw-r--r--resources/assets/javascripts/lib/instschedule.js2
-rw-r--r--resources/assets/javascripts/lib/jsupdater.js2
-rw-r--r--resources/assets/javascripts/lib/lightbox.js2
-rw-r--r--resources/assets/javascripts/lib/messages.js2
-rw-r--r--resources/assets/javascripts/lib/multi_person_search.js2
-rw-r--r--resources/assets/javascripts/lib/multi_select.js2
-rw-r--r--resources/assets/javascripts/lib/oer.js2
-rw-r--r--resources/assets/javascripts/lib/overlapping.js4
-rw-r--r--resources/assets/javascripts/lib/overlay.js2
-rw-r--r--resources/assets/javascripts/lib/qr_code.js2
-rw-r--r--resources/assets/javascripts/lib/questionnaire.js2
-rw-r--r--resources/assets/javascripts/lib/quick_search.js2
-rw-r--r--resources/assets/javascripts/lib/raumzeit.js2
-rw-r--r--resources/assets/javascripts/lib/register.js2
-rw-r--r--resources/assets/javascripts/lib/resources.js2
-rw-r--r--resources/assets/javascripts/lib/schedule.js2
-rw-r--r--resources/assets/javascripts/lib/tour.js2
-rw-r--r--resources/assets/javascripts/lib/user_filter.js2
-rw-r--r--resources/assets/javascripts/mvv.js2
-rw-r--r--resources/assets/javascripts/studip-jquery.multi-select.tweaks.js2
-rw-r--r--resources/assets/javascripts/studip-ui.js2
-rw-r--r--resources/vue-gettext.d.ts17
-rw-r--r--resources/vue/components/StudipDate.vue27
-rw-r--r--resources/vue/components/courseware/CoursewareDashboardStudents.vue481
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue3
-rw-r--r--resources/vue/components/courseware/tasks/AddFeedbackDialog.vue48
-rw-r--r--resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue222
-rw-r--r--resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue (renamed from resources/vue/components/courseware/CoursewareDashboardTasks.vue)10
-rw-r--r--resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue (renamed from resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue)59
-rw-r--r--resources/vue/components/courseware/tasks/EditFeedbackDialog.vue60
-rw-r--r--resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue (renamed from resources/vue/components/courseware/TasksApp.vue)10
-rw-r--r--resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue224
-rw-r--r--resources/vue/components/courseware/tasks/RenewalDialog.vue79
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroup.vue84
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue118
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue224
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupsDeleteDialog.vue33
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue117
-rw-r--r--resources/vue/components/courseware/tasks/task-groups-helper.js31
-rw-r--r--resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue49
-rw-r--r--resources/vue/components/stock-images/colors.js2
-rw-r--r--resources/vue/components/stock-images/filters.js2
-rw-r--r--resources/vue/courseware-index-app.js2
-rw-r--r--resources/vue/courseware-tasks-app.js50
-rw-r--r--resources/vue/mixins/courseware/task-helper.js6
-rw-r--r--resources/vue/store/AdminCoursesStore.js2
-rw-r--r--resources/vue/store/courseware/courseware-tasks.module.js75
-rw-r--r--resources/vue/store/courseware/courseware.module.js8
-rw-r--r--tsconfig.json8
-rw-r--r--webpack.common.js1
-rw-r--r--webpack.dev.js8
95 files changed, 2204 insertions, 672 deletions
diff --git a/app/controllers/course/courseware.php b/app/controllers/course/courseware.php
index af7d0e9..c1421f4 100644
--- a/app/controllers/course/courseware.php
+++ b/app/controllers/course/courseware.php
@@ -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 = $perm->have_studip_perm('tutor', Context::getId(), $user->id);
+ $this->is_teacher = $GLOBALS['perm']->have_studip_perm(
+ 'tutor',
+ Context::getId(),
+ $GLOBALS['user']->id
+ );
+
Navigation::activateItem('course/courseware/tasks');
+ PageLayout::setTitle(_('Courseware: Aufgaben'));
$this->setTasksSidebar();
}
diff --git a/db/migrations/5.5.12_add_dates_to_cw_task_groups.php b/db/migrations/5.5.12_add_dates_to_cw_task_groups.php
new file mode 100644
index 0000000..aba5ea9
--- /dev/null
+++ b/db/migrations/5.5.12_add_dates_to_cw_task_groups.php
@@ -0,0 +1,35 @@
+<?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`'
+ );
+ }
+}
diff --git a/db/migrations/5.5.12_new_external_pages.php b/db/migrations/5.5.24_new_external_pages.php
index dec973f..dec973f 100644
--- a/db/migrations/5.5.12_new_external_pages.php
+++ b/db/migrations/5.5.24_new_external_pages.php
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index d4d5bbb..4f44165 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -501,6 +501,13 @@ class RouteMap
$group->get('/courseware-task-groups/{id}', Routes\Courseware\TaskGroupsShow::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->post('/courseware-task-feedback', Routes\Courseware\TaskFeedbackCreate::class);
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index 88eb3df..2acf83e 100644
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -23,7 +23,13 @@ use User;
use Course;
/**
+ * @SuppressWarnings(PHPMD.CamelCaseParameterName)
+ * @SuppressWarnings(PHPMD.CamelCaseVariableName)
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
+ * @SuppressWarnings(PHPMD.ExcessivePublicCount)
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ * @SuppressWarnings(PHPMD.Superglobals)
* @SuppressWarnings(PHPMD.TooManyMethods)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/
@@ -306,6 +312,16 @@ class Authority
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
{
return self::canUpdateTask($user, $resource);
@@ -332,6 +348,11 @@ class Authority
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
{
return self::canCreateTasks($user, $resource->structural_element);
@@ -352,7 +373,6 @@ class Authority
return self::canCreateTaskFeedback($user, $resource);
}
-
public static function canIndexStructuralElementComments(User $user, StructuralElement $resource)
{
return self::canShowStructuralElement($user, $resource);
@@ -407,7 +427,8 @@ class Authority
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)
@@ -415,7 +436,6 @@ class Authority
return self::canUpdateStructuralElementFeedback($user, $resource);
}
-
public static function canShowTemplate(User $user, Template $resource)
{
// templates are for everybody, aren't they?
@@ -430,7 +450,7 @@ class Authority
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)
@@ -490,7 +510,7 @@ class Authority
if ($user->id === $range->id) {
return true;
}
- return $GLOBALS['perm']->have_studip_perm('tutor', $range->id ,$user->id);
+ return $GLOBALS['perm']->have_studip_perm('tutor', $range->id, $user->id);
}
public static function canSortUnit(User $user, \Range $range): bool
@@ -518,7 +538,6 @@ class Authority
return $request_user->id === $user->id;
}
-
public static function canShowClipboard(User $user, Clipboard $resource): bool
{
return $resource->user_id === $user->id;
@@ -541,7 +560,7 @@ class Authority
} else {
$structural_element = $resource->getStructuralElement();
}
-
+
return $structural_element->canEdit($user);
}
diff --git a/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php b/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php
new file mode 100644
index 0000000..2ab5ffa
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/Rel/SolversOfTaskGroup.php
@@ -0,0 +1,207 @@
+<?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)];
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
index 28c4e9c..f7357a4 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsCreate.php
@@ -65,14 +65,20 @@ class TaskGroupsCreate extends JsonApiController
if (!self::arrayHas($json, 'data.attributes.title')) {
return 'Missing `title` attribute.';
}
- if (!self::arrayHas($json, 'data.attributes.submission-date')) {
- return 'Missing `submission-date` attribute.';
+ if (!self::arrayHas($json, 'data.attributes.start-date')) {
+ return 'Missing `start-date` attribute.';
}
- $submissionDate = self::arrayGet($json, 'data.attributes.submission-date');
- if (!self::isValidTimestamp($submissionDate)) {
- return '`submission-date` is not an ISO 8601 timestamp.';
+ $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::arrayHas($json, 'data.relationships.target')) {
return 'Missing `target` relationship.';
}
@@ -165,8 +171,8 @@ class TaskGroupsCreate extends JsonApiController
$target = $this->getTargetFromJson($json);
$solverMayAddBlocks = self::arrayGet($json, 'data.attributes.solver-may-add-blocks', '');
- $submissionDate = self::arrayGet($json, 'data.attributes.submission-date', '');
- $submissionDate = self::fromISO8601($submissionDate);
+ $startDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.start-date', ''));
+ $endDate = self::fromISO8601(self::arrayGet($json, 'data.attributes.end-date', ''));
$title = self::arrayGet($json, 'data.attributes.title', '');
/** @var TaskGroup $taskGroup */
@@ -177,6 +183,8 @@ class TaskGroupsCreate extends JsonApiController
'task_template_id' => $taskTemplate->getId(),
'solver_may_add_blocks' => $solverMayAddBlocks,
'title' => $title,
+ 'start_date' => $startDate->getTimestamp(),
+ 'end_date' => $endDate->getTimestamp(),
]);
foreach ($solvers as $solver) {
@@ -184,7 +192,6 @@ class TaskGroupsCreate extends JsonApiController
'task_group_id' => $taskGroup->getId(),
'solver_id' => $solver->getId(),
'solver_type' => $this->getSolverType($solver),
- 'submission_date' => $submissionDate->getTimestamp(),
]);
// copy task template
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php
new file mode 100644
index 0000000..2faf778
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsDelete.php
@@ -0,0 +1,38 @@
+<?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);
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php
new file mode 100644
index 0000000..8662b71
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Courseware/TaskGroupsUpdate.php
@@ -0,0 +1,99 @@
+<?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;
+ }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
index f0b2ce9..26a021c 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksIndex.php
@@ -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 = [];
foreach ($taskGroups as $taskGroup) {
@@ -98,7 +99,7 @@ class TasksIndex extends JsonApiController
})
->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()) ||
('group' === $task['solver_type'] && in_array($task['solver_id'], $groupIds));
});
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
index 3728dba..33b51ad 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
@@ -13,6 +13,8 @@ use Psr\Http\Message\ServerRequestInterface as Request;
/**
* Update one Task.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
*/
class TasksUpdate extends JsonApiController
{
@@ -32,7 +34,8 @@ class TasksUpdate extends JsonApiController
throw new RecordNotFoundException();
}
$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();
}
$resource = $this->updateTask($user, $resource, $json);
@@ -66,53 +69,35 @@ class TasksUpdate extends JsonApiController
private function updateTask(\User $user, Task $resource, array $json): Task
{
- if (Authority::canDeleteTask($user, $resource)) {
- if (self::arrayHas($json, 'data.attributes.renewal')) {
- $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);
+ if (Authority::canRenewTask($user, $resource)) {
+ return $this->renewTask($resource, $json);
+ }
- $resource->renewal = $newRenewalState;
- $resource->renewal_date = $renewalDate->getTimestamp();
- }
- }
- } 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;
- }
- }
+ if (self::arrayGet($json, 'data.attributes.submitted') === true && $resource->canSubmit()) {
+ $resource->submitTask();
}
- $resource->store();
+ if (self::arrayGet($json, 'data.attributes.renewal') === 'pending') {
+ $resource->requestRenewal();
+ }
return $resource;
}
- private function canSubmit(Task $resource, string $newSubmittedState): bool
+ private function renewTask(Task $resource, array $json): Task
{
- $now = time();
- if (1 === (int) $resource->submitted || !$newSubmittedState) {
- return false;
- }
- if ('granted' === $resource->renewal) {
- return $now <= $resource->renewal_date;
- } else {
- return $now <= $resource->submission_date;
+ switch (self::arrayGet($json, 'data.attributes.renewal')) {
+ case 'declined':
+ $resource->declineRenewalRequest();
+ break;
+
+ case 'granted':
+ $resource->grantRenewalRequest(
+ self::fromISO8601(self::arrayGet($json, 'data.attributes.renewal-date'))
+ );
+ break;
}
+
+ return $resource;
}
}
diff --git a/lib/classes/JsonApi/SchemaMap.php b/lib/classes/JsonApi/SchemaMap.php
index dd74bc9..71aadf7 100644
--- a/lib/classes/JsonApi/SchemaMap.php
+++ b/lib/classes/JsonApi/SchemaMap.php
@@ -59,17 +59,17 @@ class SchemaMap
\Courseware\Clipboard::class => Schemas\Courseware\Clipboard::class,
\Courseware\Container::class => Schemas\Courseware\Container::class,
\Courseware\Instance::class => Schemas\Courseware\Instance::class,
+ \Courseware\PublicLink::class => Schemas\Courseware\PublicLink::class,
\Courseware\StructuralElement::class => Schemas\Courseware\StructuralElement::class,
\Courseware\StructuralElementComment::class => Schemas\Courseware\StructuralElementComment::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\TaskGroup::class => Schemas\Courseware\TaskGroup::class,
\Courseware\TaskFeedback::class => Schemas\Courseware\TaskFeedback::class,
+ \Courseware\TaskGroup::class => Schemas\Courseware\TaskGroup::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,
];
}
}
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php
index a0605e6..81c7a0d 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/Task.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php
@@ -2,6 +2,8 @@
namespace JsonApi\Schemas\Courseware;
+use Courseware\Task as TaskModel;
+use JsonApi\Routes\Courseware\Authority as CoursewareAuthority;
use JsonApi\Schemas\SchemaProvider;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Link;
diff --git a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
index 12dbc6c..c950671 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/TaskGroup.php
@@ -3,6 +3,7 @@
namespace JsonApi\Schemas\Courseware;
use Courseware\StructuralElement;
+use Courseware\TaskGroup as TaskGroupModel;
use JsonApi\Schemas\SchemaProvider;
use Neomerx\JsonApi\Contracts\Schema\ContextInterface;
use Neomerx\JsonApi\Schema\Identifier;
@@ -35,6 +36,8 @@ class TaskGroup extends SchemaProvider
return [
'solver-may-add-blocks' => (bool) $resource['solver_may_add_blocks'],
'title' => (string) $resource->title,
+ 'start-date' => date('c', $resource['start_date']),
+ 'end-date' => date('c', $resource['end_date']),
'mkdate' => date('c', $resource['mkdate']),
'chdate' => date('c', $resource['chdate']),
];
diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php
index 3a68d3e..d409676 100644
--- a/lib/models/Courseware/Task.php
+++ b/lib/models/Courseware/Task.php
@@ -31,7 +31,9 @@ use User;
* @property \Statusgruppen $group belongs_to \Statusgruppen
* @property \Course $course belongs_to \Course
* @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
{
@@ -80,6 +82,10 @@ class Task extends \SimpleORMap
'get' => 'getSolver',
'set' => false,
];
+ $config['additional_fields']['submission_date'] = [
+ 'get' => 'getSubmissionDate',
+ 'set' => false,
+ ];
parent::configure($config);
}
@@ -171,6 +177,11 @@ class Task extends \SimpleORMap
return null;
}
+ public function getSubmissionDate(): int
+ {
+ return $this->task_group['end_date'];
+ }
+
public function getTaskProgress(): float
{
$children = $this->structural_element->findDescendants();
@@ -185,6 +196,45 @@ class Task extends \SimpleORMap
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
{
$containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]);
diff --git a/lib/models/Courseware/TaskGroup.php b/lib/models/Courseware/TaskGroup.php
index 092edf6..6902cb3 100644
--- a/lib/models/Courseware/TaskGroup.php
+++ b/lib/models/Courseware/TaskGroup.php
@@ -2,6 +2,8 @@
namespace Courseware;
+use DBManager;
+use Statusgruppen;
use User;
/**
@@ -19,11 +21,17 @@ use User;
* @property int $task_template_id database column
* @property int $solver_may_add_blocks 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 $chdate database column
* @property \SimpleORMapCollection|Task[] $tasks has_many Task
* @property \User $lecturer belongs_to \User
* @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
{
@@ -41,6 +49,11 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
'foreign_key' => 'seminar_id',
];
+ $config['belongs_to']['target'] = [
+ 'class_name' => StructuralElement::class,
+ 'foreign_key' => 'target_id',
+ ];
+
$config['has_many']['tasks'] = [
'class_name' => Task::class,
'assoc_foreign_key' => 'task_group_id',
@@ -52,6 +65,22 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
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
{
$solvers = $this->tasks->pluck('solver');
@@ -60,20 +89,45 @@ class TaskGroup extends \SimpleORMap implements \PrivacyObject
}
/**
- * Export available data of a given user into a storage object
- * (an instance of the StoredUserData class) for that user.
+ * Returns all submitters of this TaskGroup.
*
- * @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(
- 'SELECT * FROM cw_task_groups WHERE lecturer_id = ?',
- [$storage->user_id]
+ 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']);
+ }
+ }
);
- 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']);
+ }
+
}
diff --git a/lib/models/Statusgruppen.php b/lib/models/Statusgruppen.php
index e586110..e0d2575 100644
--- a/lib/models/Statusgruppen.php
+++ b/lib/models/Statusgruppen.php
@@ -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;
+ }
}
diff --git a/resources/assets/javascripts/bootstrap/application.js b/resources/assets/javascripts/bootstrap/application.js
index c831261..a9f53df 100644
--- a/resources/assets/javascripts/bootstrap/application.js
+++ b/resources/assets/javascripts/bootstrap/application.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
import eventBus from "../lib/event-bus.ts";
/* ------------------------------------------------------------------------
diff --git a/resources/assets/javascripts/bootstrap/consultations.js b/resources/assets/javascripts/bootstrap/consultations.js
index dec8f4a..ef79d9c 100644
--- a/resources/assets/javascripts/bootstrap/consultations.js
+++ b/resources/assets/javascripts/bootstrap/consultations.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
$(document).on('click', '.consultation-delete-check:not(.ignore)', event => {
const form = $(event.target).closest('form');
diff --git a/resources/assets/javascripts/bootstrap/copyable_links.js b/resources/assets/javascripts/bootstrap/copyable_links.js
index d3675ed..521eae4 100644
--- a/resources/assets/javascripts/bootstrap/copyable_links.js
+++ b/resources/assets/javascripts/bootstrap/copyable_links.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
$(document).on('click', 'a.copyable-link', function (event) {
event.preventDefault();
diff --git a/resources/assets/javascripts/bootstrap/data_secure.js b/resources/assets/javascripts/bootstrap/data_secure.js
index a1a5ac7..1b3b7a1 100644
--- a/resources/assets/javascripts/bootstrap/data_secure.js
+++ b/resources/assets/javascripts/bootstrap/data_secure.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
/**
* Secure forms or form elements by displaying a warning on page unload if
diff --git a/resources/assets/javascripts/bootstrap/forms.js b/resources/assets/javascripts/bootstrap/forms.js
index bbc3d8a..1f4937d 100644
--- a/resources/assets/javascripts/bootstrap/forms.js
+++ b/resources/assets/javascripts/bootstrap/forms.js
@@ -1,4 +1,4 @@
-import { $gettext, $gettextInterpolate } from '../lib/gettext.js';
+import { $gettext, $gettextInterpolate } from '../lib/gettext';
// Allow fieldsets to collapse
$(document).on(
diff --git a/resources/assets/javascripts/bootstrap/multi_select.js b/resources/assets/javascripts/bootstrap/multi_select.js
index 9e817b8..5996bd7 100644
--- a/resources/assets/javascripts/bootstrap/multi_select.js
+++ b/resources/assets/javascripts/bootstrap/multi_select.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
import eventBus from "../lib/event-bus.ts";
eventBus.on('studip:set-locale', () => {
diff --git a/resources/assets/javascripts/bootstrap/mvv_difflog.js b/resources/assets/javascripts/bootstrap/mvv_difflog.js
index f21c368..8ade918 100644
--- a/resources/assets/javascripts/bootstrap/mvv_difflog.js
+++ b/resources/assets/javascripts/bootstrap/mvv_difflog.js
@@ -1,4 +1,4 @@
-import { $gettext, $gettextInterpolate } from '../lib/gettext.js';
+import { $gettext, $gettextInterpolate } from '../lib/gettext';
STUDIP.domReady(() => {
$('del.diffdel').each(function() {
diff --git a/resources/assets/javascripts/bootstrap/raumzeit.js b/resources/assets/javascripts/bootstrap/raumzeit.js
index 2140497..241105b 100644
--- a/resources/assets/javascripts/bootstrap/raumzeit.js
+++ b/resources/assets/javascripts/bootstrap/raumzeit.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
STUDIP.Dialog.handlers.header['X-Raumzeit-Update-Times'] = function(json) {
var info = $.parseJSON(json);
diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js
index 388f475..25582d4 100644
--- a/resources/assets/javascripts/bootstrap/resources.js
+++ b/resources/assets/javascripts/bootstrap/resources.js
@@ -1,4 +1,4 @@
-import {$gettext} from '../lib/gettext.js';
+import {$gettext} from '../lib/gettext';
STUDIP.ready(function () {
//Event definitions:
diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
index 8f96dcb..c106de3 100644
--- a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
+++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
/**
* This file provides a set of global handlers.
diff --git a/resources/assets/javascripts/chunks/tablesorter.js b/resources/assets/javascripts/chunks/tablesorter.js
index 9cc8b0d..047c7ce 100644
--- a/resources/assets/javascripts/chunks/tablesorter.js
+++ b/resources/assets/javascripts/chunks/tablesorter.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js'
+import { $gettext } from '../lib/gettext'
import "tablesorter/dist/js/jquery.tablesorter"
import "tablesorter/dist/js/extras/jquery.tablesorter.pager.min.js"
diff --git a/resources/assets/javascripts/chunks/vue.js b/resources/assets/javascripts/chunks/vue.js
index cf95ed3..b98cc27 100644
--- a/resources/assets/javascripts/chunks/vue.js
+++ b/resources/assets/javascripts/chunks/vue.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import Router from "vue-router";
import eventBus from '../lib/event-bus.ts';
import GetTextPlugin from 'vue-gettext';
-import { getLocale, getVueConfig } from '../lib/gettext.js';
+import { getLocale, getVueConfig } from '../lib/gettext';
import PortalVue from 'portal-vue';
import BaseComponents from '../../../vue/base-components.js';
import BaseDirectives from "../../../vue/base-directives.js";
diff --git a/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js b/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js
index 814e931..41195b0 100644
--- a/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js
+++ b/resources/assets/javascripts/cke/studip-a11y-dialog/a11y-dialog.js
@@ -1,6 +1,6 @@
import { Plugin } from '@ckeditor/ckeditor5-core';
import { add } from '@ckeditor/ckeditor5-utils/src/translation-service';
-import { $gettext } from '../../lib/gettext.js';
+import { $gettext } from '../../lib/gettext';
import A11YDialogEditing from './editing.js';
import A11YDialogUI from './ui.js';
diff --git a/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js b/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js
index a2d207f..f80d703 100644
--- a/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js
+++ b/resources/assets/javascripts/cke/studip-a11y-dialog/ui.js
@@ -1,6 +1,6 @@
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import { Plugin } from '@ckeditor/ckeditor5-core';
-import { $gettext } from '../../lib/gettext.js';
+import { $gettext } from '../../lib/gettext';
const a11yIcon =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54"><path d="M32.5,43h-11a1.5,1.5,0,0,0,0,3h11a1.5,1.5,0,0,0,0-3Z"/><path d="M31.5,48h-9a1.5,1.5,0,0,0,0,3h9a1.5,1.5,0,0,0,0-3Z"/><path d="M27,3a18.54,18.54,0,0,0-2,.11,17,17,0,0,0-6.95,31.37A2,2,0,0,1,19,36.13v3.34A1.5,1.5,0,0,0,20.5,41h13a1.5,1.5,0,0,0,1.5-1.5V36.12a2,2,0,0,1,.9-1.67A17,17,0,0,0,27,3Zm7.33,28.92A5,5,0,0,0,32,36.12V38H22V36.13a5,5,0,0,0-2.33-4.24,14,14,0,0,1,5.7-25.83A14.84,14.84,0,0,1,27,6a14,14,0,0,1,7.33,25.92Z"/><path d="M32.39,9.05A12.51,12.51,0,0,0,27.24,8a12.66,12.66,0,0,0-10.37,5.4,1.73,1.73,0,0,0,.42,2.41,1.69,1.69,0,0,0,1,.32,1.73,1.73,0,0,0,1.42-.74,9.21,9.21,0,0,1,7.54-3.93,9.08,9.08,0,0,1,3.74.8,1.73,1.73,0,1,0,1.41-3.16Z"/><path d="M17,16.31A1.73,1.73,0,0,0,15,17.58a12.38,12.38,0,0,0-.37,3,12.68,12.68,0,0,0,.28,2.67,1.74,1.74,0,0,0,1.69,1.36,1.55,1.55,0,0,0,.37,0,1.74,1.74,0,0,0,1.33-2.06A8.92,8.92,0,0,1,18,20.61a9.08,9.08,0,0,1,.27-2.2A1.74,1.74,0,0,0,17,16.31Z"/></svg>';
diff --git a/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js b/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js
index e50f8c6..0cd43e9 100644
--- a/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js
+++ b/resources/assets/javascripts/cke/studip-quote/StudipBlockQuote.js
@@ -1,6 +1,6 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import { $gettext } from '../../lib/gettext.js';
+import { $gettext } from '../../lib/gettext';
import { Command, icons } from '@ckeditor/ckeditor5-core';
const divideIcon =
diff --git a/resources/assets/javascripts/cke/wiki-link/formview.js b/resources/assets/javascripts/cke/wiki-link/formview.js
index 8d82e25..8a1525a 100644
--- a/resources/assets/javascripts/cke/wiki-link/formview.js
+++ b/resources/assets/javascripts/cke/wiki-link/formview.js
@@ -12,7 +12,7 @@ import {
addListToDropdown,
} from '@ckeditor/ckeditor5-ui';
import { FocusTracker, KeystrokeHandler, Collection, Rect, isVisible } from '@ckeditor/ckeditor5-utils';
-import { $gettext } from '../../lib/gettext.js';
+import { $gettext } from '../../lib/gettext';
export default class WikiLinkFormView extends View {
constructor(locale) {
diff --git a/resources/assets/javascripts/cke/wiki-link/ui.js b/resources/assets/javascripts/cke/wiki-link/ui.js
index a8e5f89..dba6b82 100644
--- a/resources/assets/javascripts/cke/wiki-link/ui.js
+++ b/resources/assets/javascripts/cke/wiki-link/ui.js
@@ -1,7 +1,7 @@
import { Plugin } from '@ckeditor/ckeditor5-core';
import { createDropdown } from '@ckeditor/ckeditor5-ui';
import WikiLinkFormView from './formview.js';
-import { $gettext } from '../../lib/gettext.js';
+import { $gettext } from '../../lib/gettext';
const wikiIcon =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 54"><path class="cls-1" d="M49.83,15a15.17,15.17,0,0,1-10.17,7.9,31.41,31.41,0,0,1,3.45,11.38C46.63,32.05,53.82,25.94,49.83,15ZM4.17,15c-4,10.94,3.2,17,6.72,19.28A31.41,31.41,0,0,1,14.34,22.9,15.17,15.17,0,0,1,4.17,15ZM27,16c-7.1,0-12.85,10.31-12.85,23h25.7C39.85,26.29,34.1,16,27,16Z"/></svg>';
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index e824775..8981e95 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -77,7 +77,7 @@ import Table from './lib/table.js';
import TableOfContents from './lib/table-of-contents.js';
import Tooltip from './lib/tooltip.js';
import Tour from './lib/tour.js';
-import * as Gettext from './lib/gettext.js';
+import * as Gettext from './lib/gettext';
import UserFilter from './lib/user_filter.js';
import wysiwyg from './lib/wysiwyg.js';
import ScrollToTop from './lib/scroll_to_top.js';
diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js
index bdee32d..bd16422 100644
--- a/resources/assets/javascripts/jquery-bundle.js
+++ b/resources/assets/javascripts/jquery-bundle.js
@@ -1,6 +1,6 @@
import 'expose-loader?exposes[]=$&exposes[]=jQuery!jquery';
-import { setLocale } from './lib/gettext.js';
+import { setLocale } from './lib/gettext';
import 'jquery-ui/ui/widget.js';
import 'jquery-ui/ui/position.js';
@@ -76,7 +76,7 @@ import 'blueimp-file-upload/js/jquery.iframe-transport.js';
import './jquery/autoresize.jquery.min.js';
-import { $gettext } from './lib/gettext.js';
+import { $gettext } from './lib/gettext';
// Create jQuery "plugin" that just reverses the elements' order. This is
// neccessary since the navigation is built and afterwards, we need to
diff --git a/resources/assets/javascripts/lib/admission.js b/resources/assets/javascripts/lib/admission.js
index 7cf8c88..df62bbe 100644
--- a/resources/assets/javascripts/lib/admission.js
+++ b/resources/assets/javascripts/lib/admission.js
@@ -1,7 +1,7 @@
/* ------------------------------------------------------------------------
* Anmeldeverfahren und -sets
* ------------------------------------------------------------------------ */
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
const Admission = {
diff --git a/resources/assets/javascripts/lib/big_image_handler.js b/resources/assets/javascripts/lib/big_image_handler.js
index 5130997..55e9b38 100644
--- a/resources/assets/javascripts/lib/big_image_handler.js
+++ b/resources/assets/javascripts/lib/big_image_handler.js
@@ -18,7 +18,7 @@
* @license GPL2 or any later version
* @since Stud.IP 3.4
*/
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
var pixelRatio = window.devicePixelRatio || 1,
dataAttribute = 'big-image-handled';
diff --git a/resources/assets/javascripts/lib/calendar.js b/resources/assets/javascripts/lib/calendar.js
index 2f1cd67..2d995b5 100644
--- a/resources/assets/javascripts/lib/calendar.js
+++ b/resources/assets/javascripts/lib/calendar.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
import eventBus from "./event-bus.ts";
eventBus.on('studip:set-locale', () => {
diff --git a/resources/assets/javascripts/lib/dialog.js b/resources/assets/javascripts/lib/dialog.js
index 8c22d1c..b5cab54 100644
--- a/resources/assets/javascripts/lib/dialog.js
+++ b/resources/assets/javascripts/lib/dialog.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
import parseOptions from './parse_options.js';
import extractCallback from './extract_callback.js';
import Overlay from './overlay.js';
diff --git a/resources/assets/javascripts/lib/files.js b/resources/assets/javascripts/lib/files.js
index 7b628f6..d05112d 100644
--- a/resources/assets/javascripts/lib/files.js
+++ b/resources/assets/javascripts/lib/files.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
import FilesTable from '../../../vue/components/FilesTable.vue';
diff --git a/resources/assets/javascripts/lib/folders.js b/resources/assets/javascripts/lib/folders.js
index ced430d..6cd23c5 100644
--- a/resources/assets/javascripts/lib/folders.js
+++ b/resources/assets/javascripts/lib/folders.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
const Folders = {
diff --git a/resources/assets/javascripts/lib/forum.js b/resources/assets/javascripts/lib/forum.js
index c2f0d7c..385ec12 100644
--- a/resources/assets/javascripts/lib/forum.js
+++ b/resources/assets/javascripts/lib/forum.js
@@ -1,4 +1,4 @@
-import { $gettext } from "./gettext.js";
+import { $gettext } from "./gettext";
import eventBus from "./event-bus.ts";
eventBus.on('studip:set-locale', () => {
diff --git a/resources/assets/javascripts/lib/gettext.js b/resources/assets/javascripts/lib/gettext.ts
index 5742466..23daaaa 100644
--- a/resources/assets/javascripts/lib/gettext.js
+++ b/resources/assets/javascripts/lib/gettext.ts
@@ -1,6 +1,25 @@
import { translate } from 'vue-gettext';
-import defaultTranslations from '../../../../locale/de/LC_MESSAGES/js-resources.json';
-import eventBus from './event-bus.ts';
+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';
@@ -24,7 +43,7 @@ async function setLocale(locale = getInitialLocale()) {
state.locale = locale;
if (state.translations[state.locale] === null) {
- const translations = await getTranslations(state.locale);
+ const translations: TranslationDict = await getTranslations(state.locale);
state.translations[state.locale] = translations;
}
@@ -43,7 +62,7 @@ function getVueConfig() {
memo[lang] = name;
return memo;
- }, {});
+ }, {} as StringDict);
return {
availableLanguages,
@@ -55,11 +74,11 @@ function getVueConfig() {
}
function getInitialState() {
- const translations = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
+ const translations: TranslationDicts = Object.entries(getInstalledLanguages()).reduce((memo, [lang]) => {
memo[lang] = lang === DEFAULT_LANG ? defaultTranslations : null;
return memo;
- }, {});
+ }, {} as TranslationDicts);
return {
locale: DEFAULT_LANG,
@@ -77,11 +96,11 @@ function getInitialLocale() {
return DEFAULT_LANG;
}
-function getInstalledLanguages() {
+function getInstalledLanguages(): InstalledLanguages {
return window?.STUDIP?.INSTALLED_LANGUAGES ?? { [DEFAULT_LANG]: { name: DEFAULT_LANG_NAME, selected: true } };
}
-async function getTranslations(locale) {
+async function getTranslations(locale: string): Promise<TranslationDict> {
try {
const language = locale.split(/[_-]/)[0];
const translation = await import(`../../../../locale/${language}/LC_MESSAGES/js-resources.json`);
diff --git a/resources/assets/javascripts/lib/instschedule.js b/resources/assets/javascripts/lib/instschedule.js
index af438c2..d925bfb 100644
--- a/resources/assets/javascripts/lib/instschedule.js
+++ b/resources/assets/javascripts/lib/instschedule.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
const Instschedule = {
diff --git a/resources/assets/javascripts/lib/jsupdater.js b/resources/assets/javascripts/lib/jsupdater.js
index 7888f29..5069af0 100644
--- a/resources/assets/javascripts/lib/jsupdater.js
+++ b/resources/assets/javascripts/lib/jsupdater.js
@@ -10,7 +10,7 @@
*
* Refer to the according function definitions for further info.
* ------------------------------------------------------------------------ */
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
let active = false;
diff --git a/resources/assets/javascripts/lib/lightbox.js b/resources/assets/javascripts/lib/lightbox.js
index 134cfca..09bfda2 100644
--- a/resources/assets/javascripts/lib/lightbox.js
+++ b/resources/assets/javascripts/lib/lightbox.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
function sprintf(string) {
diff --git a/resources/assets/javascripts/lib/messages.js b/resources/assets/javascripts/lib/messages.js
index dbb2793..7ce5328 100644
--- a/resources/assets/javascripts/lib/messages.js
+++ b/resources/assets/javascripts/lib/messages.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Markup from './markup.js';
const Messages = {
diff --git a/resources/assets/javascripts/lib/multi_person_search.js b/resources/assets/javascripts/lib/multi_person_search.js
index f5ba046..b876bc9 100644
--- a/resources/assets/javascripts/lib/multi_person_search.js
+++ b/resources/assets/javascripts/lib/multi_person_search.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
const MultiPersonSearch = {
init: function() {
diff --git a/resources/assets/javascripts/lib/multi_select.js b/resources/assets/javascripts/lib/multi_select.js
index b4abeb9..6c1b387 100644
--- a/resources/assets/javascripts/lib/multi_select.js
+++ b/resources/assets/javascripts/lib/multi_select.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
/**
* Turns a select-box into an easy to use multiple select-box
diff --git a/resources/assets/javascripts/lib/oer.js b/resources/assets/javascripts/lib/oer.js
index 112d155..17f0186 100644
--- a/resources/assets/javascripts/lib/oer.js
+++ b/resources/assets/javascripts/lib/oer.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
const OER = {
periodicalPushData: function () {
diff --git a/resources/assets/javascripts/lib/overlapping.js b/resources/assets/javascripts/lib/overlapping.js
index 73ab32f..a6aa4b8 100644
--- a/resources/assets/javascripts/lib/overlapping.js
+++ b/resources/assets/javascripts/lib/overlapping.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
const Overlapping = {
@@ -91,4 +91,4 @@ const Overlapping = {
}
};
-export default Overlapping; \ No newline at end of file
+export default Overlapping;
diff --git a/resources/assets/javascripts/lib/overlay.js b/resources/assets/javascripts/lib/overlay.js
index 52d1c94..ffe8ed1 100644
--- a/resources/assets/javascripts/lib/overlay.js
+++ b/resources/assets/javascripts/lib/overlay.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
const Overlay = {
delay: 300,
diff --git a/resources/assets/javascripts/lib/qr_code.js b/resources/assets/javascripts/lib/qr_code.js
index 3db4fd8..ada0cb3 100644
--- a/resources/assets/javascripts/lib/qr_code.js
+++ b/resources/assets/javascripts/lib/qr_code.js
@@ -1,4 +1,4 @@
-import { $gettext } from "./gettext.js";
+import { $gettext } from "./gettext";
import Dialog from "./dialog.js";
const QRCode = {
diff --git a/resources/assets/javascripts/lib/questionnaire.js b/resources/assets/javascripts/lib/questionnaire.js
index 8fbbbb0..2bca8c6 100644
--- a/resources/assets/javascripts/lib/questionnaire.js
+++ b/resources/assets/javascripts/lib/questionnaire.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
import md5 from 'md5';
//import html2canvas from "html2canvas";
//import {jsPDF} from "jspdf";
diff --git a/resources/assets/javascripts/lib/quick_search.js b/resources/assets/javascripts/lib/quick_search.js
index 806debd..627bffa 100644
--- a/resources/assets/javascripts/lib/quick_search.js
+++ b/resources/assets/javascripts/lib/quick_search.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
/* ------------------------------------------------------------------------
* QuickSearch inputs
diff --git a/resources/assets/javascripts/lib/raumzeit.js b/resources/assets/javascripts/lib/raumzeit.js
index 5cd5e55..c28dbae 100644
--- a/resources/assets/javascripts/lib/raumzeit.js
+++ b/resources/assets/javascripts/lib/raumzeit.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
const Raumzeit = {
disableBookableRooms: function(icon) {
diff --git a/resources/assets/javascripts/lib/register.js b/resources/assets/javascripts/lib/register.js
index da81132..de7b666 100644
--- a/resources/assets/javascripts/lib/register.js
+++ b/resources/assets/javascripts/lib/register.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
const register = {
re_username: null,
diff --git a/resources/assets/javascripts/lib/resources.js b/resources/assets/javascripts/lib/resources.js
index 2375eee..9acb2e3 100644
--- a/resources/assets/javascripts/lib/resources.js
+++ b/resources/assets/javascripts/lib/resources.js
@@ -1,4 +1,4 @@
-import { $gettext } from '../lib/gettext.js';
+import { $gettext } from '../lib/gettext';
class Resources
{
diff --git a/resources/assets/javascripts/lib/schedule.js b/resources/assets/javascripts/lib/schedule.js
index b7c9d37..f3e5123 100644
--- a/resources/assets/javascripts/lib/schedule.js
+++ b/resources/assets/javascripts/lib/schedule.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Calendar from './calendar.js';
import Dialog from './dialog.js';
diff --git a/resources/assets/javascripts/lib/tour.js b/resources/assets/javascripts/lib/tour.js
index b93be07..8094b2b 100644
--- a/resources/assets/javascripts/lib/tour.js
+++ b/resources/assets/javascripts/lib/tour.js
@@ -1,4 +1,4 @@
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
/* ------------------------------------------------------------------------
* Stud.IP Tour
diff --git a/resources/assets/javascripts/lib/user_filter.js b/resources/assets/javascripts/lib/user_filter.js
index 450af34..25b8488 100644
--- a/resources/assets/javascripts/lib/user_filter.js
+++ b/resources/assets/javascripts/lib/user_filter.js
@@ -1,7 +1,7 @@
/* ------------------------------------------------------------------------
* Bedingungen zur Auswahl von Stud.IP-Nutzern
* ------------------------------------------------------------------------ */
-import { $gettext } from './gettext.js';
+import { $gettext } from './gettext';
import Dialog from './dialog.js';
const UserFilter = {
diff --git a/resources/assets/javascripts/mvv.js b/resources/assets/javascripts/mvv.js
index 12d2653..a339624 100644
--- a/resources/assets/javascripts/mvv.js
+++ b/resources/assets/javascripts/mvv.js
@@ -1,4 +1,4 @@
-import { $gettext } from './lib/gettext.js';
+import { $gettext } from './lib/gettext';
jQuery(function ($) {
$(document).on('click', 'a.mvv-load-in-new-row', function () {
diff --git a/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js
index 2462ff9..adde0ce 100644
--- a/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js
+++ b/resources/assets/javascripts/studip-jquery.multi-select.tweaks.js
@@ -1,4 +1,4 @@
-import { $gettext } from './lib/gettext.js';
+import { $gettext } from './lib/gettext';
/**
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
index 60a3cfa..f581295 100644
--- a/resources/assets/javascripts/studip-ui.js
+++ b/resources/assets/javascripts/studip-ui.js
@@ -1,4 +1,4 @@
-import { $gettext } from './lib/gettext.js';
+import { $gettext } from './lib/gettext';
import eventBus from "./lib/event-bus.ts";
/**
diff --git a/resources/vue-gettext.d.ts b/resources/vue-gettext.d.ts
new file mode 100644
index 0000000..b3f4c66
--- /dev/null
+++ b/resources/vue-gettext.d.ts
@@ -0,0 +1,17 @@
+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;
+}
diff --git a/resources/vue/components/StudipDate.vue b/resources/vue/components/StudipDate.vue
new file mode 100644
index 0000000..2e30b9d
--- /dev/null
+++ b/resources/vue/components/StudipDate.vue
@@ -0,0 +1,27 @@
+<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>
diff --git a/resources/vue/components/courseware/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/CoursewareDashboardStudents.vue
deleted file mode 100644
index bac31a6..0000000
--- a/resources/vue/components/courseware/CoursewareDashboardStudents.vue
+++ /dev/null
@@ -1,481 +0,0 @@
-<template>
- <div class="cw-dashboard-students-wrapper">
- <table v-if="tasks.length > 0" class="default">
- <colgroup>
- <col />
- </colgroup>
- <thead>
- <tr class="sortable">
- <th>{{ $gettext('Status') }}</th>
- <th :class="getSortClass('task-title')" @click="sort('task-title')">
- {{ $gettext('Aufgabentitel') }}
- </th>
- <th :class="getSortClass('solver-name')" @click="sort('solver-name')">
- {{ $gettext('Teilnehmende/Gruppen') }}
- </th>
- <th class="responsive-hidden" :class="getSortClass('page-title')" @click="sort('page-title')">
- {{ $gettext('Seite') }}
- </th>
- <th :class="getSortClass('progress')" @click="sort('progress')">
- {{ $gettext('bearbeitet') }}
- </th>
- <th :class="getSortClass('submission-date')" @click="sort('submission-date')">
- {{ $gettext('Abgabefrist') }}
- </th>
- <th>{{ $gettext('Abgabe') }}</th>
- <th class="responsive-hidden renewal">{{ $gettext('Verlängerungsanfrage') }}</th>
- <th class="responsive-hidden feedback">{{ $gettext('Anmerkungen') }}</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="{ task, taskGroup, status, element, user, group, feedback } in tasks" :key="task.id">
- <td>
- <studip-icon
- v-if="status.shape !== undefined"
- :shape="status.shape"
- :role="status.role"
- :title="status.description"
- aria-hidden="true"
- />
- <span class="sr-only">{{ status.description }}</span>
- </td>
- <td>
- {{ taskGroup && taskGroup.attributes.title }}
- </td>
- <td>
- <span v-if="user">
- <studip-icon
- shape="person2"
- role="info"
- aria-hidden="true"
- :title="$gettext('Teilnehmende Person')"
- />
- <span class="sr-only">{{ $gettext('Teilnehmende Person') }}</span>
- {{ user.attributes['formatted-name'] }}
-
- </span>
- <span v-if="group">
- <studip-icon
- shape="group2"
- role="info"
- aria-hidden="true"
- :title="$gettext('Gruppe')"
- />
- <span class="sr-only">{{ $gettext('Gruppe') }}</span>
- {{ group.attributes['name'] }}
-
- </span>
- </td>
- <td class="responsive-hidden">
- <a v-if="task.attributes.submitted" :href="getLinkToElement(element)">
- {{ element.attributes.title }}
- </a>
- <span v-else>{{ element.attributes.title }}</span>
- </td>
- <td>{{ task.attributes?.progress?.toFixed(2) || '-.--' }}%</td>
- <td>{{ getReadableDate(task.attributes['submission-date']) }}</td>
- <td>
- <studip-icon v-if="task.attributes.submitted" shape="accept" role="status-green" />
- </td>
- <td class="responsive-hidden">
- <button
- v-show="task.attributes.renewal === 'pending'"
- class="button"
- @click="solveRenewalRequest(task)"
- >
- {{ $gettext('Anfrage bearbeiten') }}
- </button>
- <span v-show="task.attributes.renewal === 'declined'">
- <studip-icon shape="decline" role="status-red" />
- {{ $gettext('Anfrage abgelehnt') }}
- </span>
- <span v-show="task.attributes.renewal === 'granted'">
- {{ $gettext('verlängert bis') }}:
- {{ getReadableDate(task.attributes['renewal-date']) }}
- </span>
- <studip-icon
- v-if="task.attributes.renewal === 'declined' || task.attributes.renewal === 'granted'"
- :title="$gettext('Anfrage bearbeiten')"
- class="edit"
- shape="edit"
- role="clickable"
- @click="solveRenewalRequest(task)"
- />
- </td>
- <td class="responsive-hidden">
- <span
- v-if="feedback"
- :title="
- $gettext('Anmerkung geschrieben am:') +
- ' ' +
- getReadableDate(feedback.attributes['chdate'])
- "
- >
- <studip-icon shape="accept" role="status-green" />
- {{ $gettext('Anmerkung gegeben') }}
- <studip-icon
- :title="$gettext('Anmerkung bearbeiten')"
- class="edit"
- shape="edit"
- role="clickable"
- @click="editFeedback(feedback)"
- />
- </span>
-
- <button
- v-show="!feedback && task.attributes.submitted"
- class="button"
- @click="addFeedback(task)"
- >
- {{ $gettext('Anmerkung geben') }}
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- <div v-else>
- <courseware-companion-box
- mood="pointing"
- :msgCompanion="$gettext('Es wurden bisher keine Aufgaben gestellt.')"
- >
- </courseware-companion-box>
- </div>
- <studip-dialog
- v-if="showRenewalDialog"
- :title="text.renewalDialog.title"
- :confirmText="text.renewalDialog.confirm"
- confirmClass="accept"
- :closeText="text.renewalDialog.close"
- closeClass="cancel"
- height="350"
- @close="
- showRenewalDialog = false;
- currentDialogTask = {};
- "
- @confirm="updateRenewal"
- >
- <template v-slot:dialogContent>
- <form class="default" @submit.prevent="">
- <label>
- {{ $gettext('Fristverlängerung') }}
- <select v-model="currentDialogTask.attributes.renewal">
- <option value="declined">
- {{ $gettext('ablehnen') }}
- </option>
- <option value="granted">
- {{ $gettext('gewähren') }}
- </option>
- </select>
- </label>
- <label v-if="currentDialogTask.attributes.renewal === 'granted'">
- {{ $gettext('neue Frist') }}
- <courseware-date-input v-model="currentDialogTask.attributes['renewal-date']" class="size-l" />
- </label>
- </form>
- </template>
- </studip-dialog>
- <studip-dialog
- v-if="showEditFeedbackDialog"
- :title="text.editFeedbackDialog.title"
- :confirmText="text.editFeedbackDialog.confirm"
- confirmClass="accept"
- :closeText="text.editFeedbackDialog.close"
- closeClass="cancel"
- height="420"
- @close="
- showEditFeedbackDialog = false;
- currentDialogFeedback = {};
- "
- @confirm="updateFeedback"
- >
- <template v-slot:dialogContent>
- <courseware-companion-box
- v-if="currentDialogFeedback.attributes.content === ''"
- mood="pointing"
- :msgCompanion="
- $gettext('Sie haben keine Anmerkungen geschrieben, beim Speichern wird diese Anmerkung gelöscht!')
- "
- />
- <form class="default" @submit.prevent="">
- <label>
- {{ $gettext('Anmerkung') }}
- <textarea v-model="currentDialogFeedback.attributes.content" />
- </label>
- </form>
- </template>
- </studip-dialog>
- <studip-dialog
- v-if="showAddFeedbackDialog"
- :title="text.addFeedbackDialog.title"
- :confirmText="text.addFeedbackDialog.confirm"
- confirmClass="accept"
- :closeText="text.addFeedbackDialog.close"
- closeClass="cancel"
- @close="
- showAddFeedbackDialog = false;
- currentDialogFeedback = {};
- "
- @confirm="createFeedback"
- >
- <template v-slot:dialogContent>
- <form class="default" @submit.prevent="">
- <label>
- {{ $gettext('Anmerkung') }}
- <textarea v-model="currentDialogFeedback.attributes.content" />
- </label>
- </form>
- </template>
- </studip-dialog>
- <courseware-tasks-dialog-distribute v-if="showTasksDistributeDialog" @newtask="reloadTasks"/>
- </div>
-</template>
-
-<script>
-import StudipIcon from './../StudipIcon.vue';
-import StudipDialog from './../StudipDialog.vue';
-import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
-import CoursewareDateInput from './layouts/CoursewareDateInput.vue';
-import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
-import taskHelperMixin from '../../mixins/courseware/task-helper.js';
-import { mapActions, mapGetters } from 'vuex';
-
-
-export default {
- name: 'courseware-dashboard-students',
- mixins: [taskHelperMixin],
- components: {
- CoursewareCompanionBox,
- CoursewareDateInput,
- StudipIcon,
- StudipDialog,
- CoursewareTasksDialogDistribute,
- },
- data() {
- return {
- showRenewalDialog: false,
- showAddFeedbackDialog: false,
- showEditFeedbackDialog: false,
- currentDialogTask: {},
- currentDialogFeedback: {},
- text: {
- renewalDialog: {
- title: this.$gettext('Verlängerungsanfrage bearbeiten'),
- confirm: this.$gettext('Speichern'),
- close: this.$gettext('Schließen'),
- },
- editFeedbackDialog: {
- title: this.$gettext('Anmerkung zur Aufgabe ändern'),
- confirm: this.$gettext('Speichern'),
- close: this.$gettext('Schließen'),
- },
- addFeedbackDialog: {
- title: this.$gettext('Anmerkung zur Aufgabe erstellen'),
- confirm: this.$gettext('Speichern'),
- close: this.$gettext('Schließen'),
- },
- },
- sortBy: 'task-title',
- sortASC: true,
- };
- },
- computed: {
- ...mapGetters({
- context: 'context',
- allTasks: 'courseware-tasks/all',
- userById: 'users/byId',
- statusGroupById: 'status-groups/byId',
- getElementById: 'courseware-structural-elements/byId',
- getFeedbackById: 'courseware-task-feedback/byId',
- relatedTaskGroups: 'courseware-task-groups/related',
- showTasksDistributeDialog: 'showTasksDistributeDialog'
- }),
- tasks() {
- const tasks = this.allTasks.map((task) => {
- const result = {
- task,
- taskGroup: this.relatedTaskGroups({ parent: task, relationship: 'task-group' }),
- status: this.getStatus(task),
- element: this.getElementById({ id: task.relationships['structural-element'].data.id }),
- user: null,
- group: null,
- feedback: null,
- solverName: null
- };
- let solver = task.relationships.solver.data;
- if (solver.type === 'users') {
- result.user = this.userById({ id: solver.id });
- result.solverName = result.user.attributes['formatted-name'];
- }
- if (solver.type === 'status-groups') {
- result.group = this.statusGroupById({ id: solver.id });
- result.solverName = result.group.attributes['name'];
- }
-
- const feedbackId = task.relationships['task-feedback'].data?.id;
- if (feedbackId) {
- result.feedback = this.getFeedbackById({ id: feedbackId });
- }
-
- return result;
- });
-
- return this.sortTasks(tasks);
- },
- managerUrl() {
- return STUDIP.URLHelper.getURL('dispatch.php/course/courseware/manager', {cid: this.context.id});
- }
- },
- methods: {
- ...mapActions({
- updateTask: 'updateTask',
- createTaskFeedback: 'createTaskFeedback',
- updateTaskFeedback: 'updateTaskFeedback',
- deleteTaskFeedback: 'deleteTaskFeedback',
- loadRemoteCoursewareStructure: 'loadRemoteCoursewareStructure',
- copyStructuralElement: 'copyStructuralElement',
- companionSuccess: 'companionSuccess',
- companionError: 'companionError',
- loadAllTasks: 'courseware-tasks/loadAll'
- }),
- addFeedback(task) {
- this.currentDialogFeedback.attributes = {};
- this.currentDialogFeedback.attributes.content = '';
- this.currentDialogFeedback.relationships = {};
- this.currentDialogFeedback.relationships.task = {};
- this.currentDialogFeedback.relationships.task.data = {};
- this.currentDialogFeedback.relationships.task.data.id = task.id;
- this.currentDialogFeedback.relationships.task.data.type = task.type;
- this.showAddFeedbackDialog = true;
- },
- createFeedback() {
- if (this.currentDialogFeedback.attributes.content === '') {
- this.companionError({
- info: this.$gettext('Bitte schreiben Sie eine Anmerkung.'),
- });
- return false;
- }
- this.showAddFeedbackDialog = false;
- this.createTaskFeedback({
- taskFeedback: this.currentDialogFeedback,
- });
- this.currentDialogFeedback = {};
- },
- editFeedback(feedback) {
- this.currentDialogFeedback = _.cloneDeep(feedback);
- this.showEditFeedbackDialog = true;
- },
- async updateFeedback() {
- this.showEditFeedbackDialog = false;
- let attributes = {};
- attributes.content = this.currentDialogFeedback.attributes.content;
- if (attributes.content === '') {
- await this.deleteTaskFeedback({
- taskFeedbackId: this.currentDialogFeedback.id,
- });
- this.companionSuccess({
- info: this.$gettext('Anmerkung wurde gelöscht.'),
- });
- } else {
- await this.updateTaskFeedback({
- attributes: attributes,
- taskFeedbackId: this.currentDialogFeedback.id,
- });
- this.companionSuccess({
- info: this.$gettext('Anmerkung wurde gespeichert.'),
- });
- }
-
- this.currentDialogFeedback = {};
- },
- solveRenewalRequest(task) {
- this.currentDialogTask = _.cloneDeep(task);
- this.currentDialogTask.attributes['renewal-date'] = new Date().toISOString();
- this.showRenewalDialog = true;
- },
- updateRenewal() {
- this.showRenewalDialog = false;
- let attributes = {};
- attributes.renewal = this.currentDialogTask.attributes.renewal;
- if (attributes.renewal === 'granted') {
- attributes['renewal-date'] = new Date(this.currentDialogTask.attributes['renewal-date'] || Date.now()).toISOString();
- }
-
- this.updateTask({
- attributes: attributes,
- taskId: this.currentDialogTask.id,
- });
- this.currentDialogTask = {};
- },
- reloadTasks() {
- this.loadAllTasks({
- options: {
- 'filter[cid]': this.context.id,
- include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer'
- }
- });
- },
- getSortClass(col) {
- if (col === this.sortBy) {
- return this.sortASC ? 'sortasc' : 'sortdesc';
- }
- },
- sort(sortBy) {
- if (this.sortBy === sortBy) {
- this.sortASC = !this.sortASC;
- } else {
- this.sortBy = sortBy;
- }
- },
- sortTasks(tasks) {
- switch (this.sortBy) {
- case 'task-title':
- tasks = tasks.sort((a, b) => {
- if (this.sortASC) {
- return a.taskGroup.attributes.title < b.taskGroup.attributes.title ? -1 : 1;
- } else {
- return a.taskGroup.attributes.title > b.taskGroup.attributes.title ? -1 : 1;
- }
- });
- break;
- case 'solver-name':
- tasks = tasks.sort((a, b) => {
- if (this.sortASC) {
- return a.solverName < b.solverName ? -1 : 1;
- } else {
- return a.solverName > b.solverName ? -1 : 1;
- }
- });
- break;
- case 'page-title':
- tasks = tasks.sort((a, b) => {
- if (this.sortASC) {
- return a.element.attributes.title < b.element.attributes.title ? -1 : 1;
- } else {
- return a.element.attributes.title > b.element.attributes.title ? -1 : 1;
- }
- });
- break;
- case 'progress':
- tasks = tasks.sort((a, b) => {
- if (this.sortASC) {
- return a.task.attributes.progress < b.task.attributes.progress ? -1 : 1;
- } else {
- return a.task.attributes.progress > b.task.attributes.progress ? -1 : 1;
- }
- });
- break;
- case 'submission-date':
- tasks = tasks.sort((a, b) => {
- if (this.sortASC) {
- return new Date(a.task.attributes['submission-date']) - new Date(b.task.attributes['submission-date']);
- } else {
- return new Date(b.task.attributes['submission-date']) - new Date(a.task.attributes['submission-date']);
- }
- });
- break;
- }
- return tasks;
- },
- },
-};
-</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
index eefc0fb..e79df40 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCollapsibleBox.vue
@@ -2,7 +2,8 @@
<div class="cw-collapsible" :class="{ 'cw-collapsible-open': isOpen }">
<a href="#" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen">
<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>
</a>
<div v-if="isOpen" class="cw-collapsible-content" :class="{ 'cw-collapsible-content-open': isOpen }">
diff --git a/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue b/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue
new file mode 100644
index 0000000..2d7c281
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/AddFeedbackDialog.vue
@@ -0,0 +1,48 @@
+<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>
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
new file mode 100644
index 0000000..426b0cb
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardStudents.vue
@@ -0,0 +1,222 @@
+<template>
+ <div class="cw-dashboard-students-wrapper">
+ <CoursewareRibbon :isContentBar="true" :showToolbarButton="false">
+ <template #buttons>
+ <router-link :to="{ name: 'task-groups-index' }">
+ <StudipIcon shape="category-task" :size="24" />
+ </router-link>
+ </template>
+ <template #breadcrumbList>
+ <li>
+ {{ $gettext('Aufgaben') }}
+ </li>
+ </template>
+ </CoursewareRibbon>
+ <table class="default" v-if="taskGroups.length">
+ <thead>
+ <tr class="sortable">
+ <th>
+ {{ $gettext('Status') }}
+ </th>
+ <th :class="getSortClass('task-group-title')" @click="sort('task-group-title')">
+ {{ $gettext('Titel') }}
+ </th>
+ <th :class="getSortClass('end-date')" @click="sort('end-date')">
+ {{ $gettext('Bearbeitungszeit') }}
+ </th>
+ <th class="actions">{{ $gettext('Aktionen') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(taskGroup, index) in sortedTaskGroups" :key="index">
+ <td>
+ <StudipIcon
+ :shape="status(taskGroup).shape"
+ :role="status(taskGroup).role"
+ :title="status(taskGroup).description"
+ aria-hidden="true"
+ />
+ <span class="sr-only">{{ status(taskGroup).description }}</span>
+ </td>
+ <td>
+ <router-link :to="{ name: 'task-groups-show', params: { id: taskGroup.id } }">{{
+ taskGroup.attributes.title
+ }}</router-link>
+ </td>
+ <td>
+ <StudipDate :date="new Date(taskGroup.attributes['start-date'])" /> - <StudipDate
+ :date="new Date(taskGroup.attributes['end-date'])"
+ />
+ </td>
+ <td class="actions">
+ <StudipActionMenu
+ :items="getTaskGroupMenuItems(taskGroup)"
+ @addsolvers="onShowAddSolvers(taskGroup)"
+ @deadline="onShowModifyDeadline(taskGroup)"
+ @delete="onShowDeleteDialog(taskGroup)"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <CompanionBox v-else-if="!tasksLoading" :msgCompanion="$gettext('Es wurden noch keine Aufgaben verteilt.')">
+ <template #companionActions>
+ <button @click="setShowTasksDistributeDialog(true)" type="button" class="button">
+ {{ $gettext('Aufgabe verteilen') }}
+ </button>
+ </template>
+ </CompanionBox>
+
+ <TaskGroupsAddSolversDialog v-if="showTaskGroupsAddSolversDialog" :taskGroup="selectedTaskGroup" @newtask="reloadTasks" />
+ <TaskGroupsDeleteDialog v-if="showTaskGroupsDeleteDialog" :taskGroup="selectedTaskGroup" />
+ <TaskGroupsModifyDeadlineDialog v-if="showTaskGroupsModifyDeadlineDialog" :taskGroup="selectedTaskGroup" />
+ <CoursewareTasksDialogDistribute v-if="showTasksDistributeDialog" @newtask="reloadTasks" />
+ </div>
+</template>
+
+<script>
+import _ from 'lodash';
+import { mapActions, mapGetters } from 'vuex';
+import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareRibbon from '../structural-element/CoursewareRibbon.vue';
+import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
+import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipDate from '../../StudipDate.vue';
+import StudipIcon from '../../StudipIcon.vue';
+import TaskGroupsAddSolversDialog from './TaskGroupsAddSolversDialog.vue';
+import TaskGroupsDeleteDialog from './TaskGroupsDeleteDialog.vue';
+import TaskGroupsModifyDeadlineDialog from './TaskGroupsModifyDeadlineDialog.vue';
+import { getStatus } from './task-groups-helper.js';
+
+export default {
+ name: 'courseware-dashboard-students',
+ components: {
+ CompanionBox,
+ CoursewareRibbon,
+ CoursewareTasksDialogDistribute,
+ StudipActionMenu,
+ StudipDate,
+ StudipIcon,
+ TaskGroupsAddSolversDialog,
+ TaskGroupsDeleteDialog,
+ TaskGroupsModifyDeadlineDialog,
+ },
+ data: () => ({
+ selectedTaskGroup: null,
+ sortBy: 'end-date',
+ sortAsc: false,
+ }),
+ computed: {
+ ...mapGetters({
+ context: 'context',
+ showTaskGroupsAddSolversDialog: 'tasks/showTaskGroupsAddSolversDialog',
+ showTaskGroupsDeleteDialog: 'tasks/showTaskGroupsDeleteDialog',
+ showTaskGroupsModifyDeadlineDialog: 'tasks/showTaskGroupsModifyDeadlineDialog',
+ showTasksDistributeDialog: 'tasks/showTasksDistributeDialog',
+ taskGroupsByCid: 'tasks/taskGroupsByCid',
+ tasksLoading: 'courseware-tasks/isLoading',
+ }),
+ sortedTaskGroups() {
+ const sorters = {
+ 'task-group-title': (taskGroup) => taskGroup.attributes.title,
+ 'end-date': (taskGroup) => new Date(taskGroup.attributes['end-date']),
+ };
+
+ return _.chain(this.taskGroups)
+ .sortBy([sorters[this.sortBy]])
+ .thru((sorted) => (this.sortAsc ? sorted : _.reverse(sorted)))
+ .value();
+ },
+ taskGroups() {
+ return this.taskGroupsByCid(this.context.id);
+ },
+ },
+ methods: {
+ ...mapActions({
+ loadAllTasks: 'courseware-tasks/loadAll',
+ setShowTaskGroupsAddSolversDialog: 'tasks/setShowTaskGroupsAddSolversDialog',
+ setShowTaskGroupsDeleteDialog: 'tasks/setShowTaskGroupsDeleteDialog',
+ setShowTaskGroupsModifyDeadlineDialog: 'tasks/setShowTaskGroupsModifyDeadlineDialog',
+ setShowTasksDistributeDialog: 'tasks/setShowTasksDistributeDialog',
+ }),
+ getSortClass(col) {
+ if (col === this.sortBy) {
+ return this.sortAsc ? 'sortasc' : 'sortdesc';
+ }
+ return '';
+ },
+ getTaskGroupMenuItems(taskGroup) {
+ let menuItems = [];
+
+ const isBeforeEndDate = new Date() < new Date(taskGroup.attributes['end-date']);
+ if (isBeforeEndDate) {
+ menuItems.push({
+ id: 'add-solvers',
+ label: this.$gettext('Teilnehmende hinzufügen'),
+ icon: 'add',
+ emit: 'addsolvers'
+ });
+ menuItems.push({
+ id: 'modify-deadline',
+ label: this.$gettext('Bearbeitungszeit verlängern'),
+ icon: 'date',
+ emit: 'deadline'
+ });
+ }
+
+ menuItems.push({
+ id: 'delete',
+ label: this.$gettext('Aufgabe löschen'),
+ icon: 'trash',
+ emit: 'delete',
+ });
+
+ return menuItems;
+ },
+ onShowAddSolvers(taskGroup) {
+ this.selectedTaskGroup = taskGroup;
+ this.setShowTaskGroupsAddSolversDialog(true);
+ },
+ onShowDeleteDialog(taskGroup) {
+ this.selectedTaskGroup = taskGroup;
+ this.setShowTaskGroupsDeleteDialog(true);
+ },
+ onShowModifyDeadline(taskGroup) {
+ this.selectedTaskGroup = taskGroup;
+ this.setShowTaskGroupsModifyDeadlineDialog(true);
+ },
+ reloadTasks() {
+ this.loadAllTasks({
+ options: {
+ 'filter[cid]': this.context.id,
+ include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer',
+ },
+ });
+ },
+ sort(sortBy) {
+ if (this.sortBy === sortBy) {
+ this.sortAsc = !this.sortAsc;
+ } else {
+ this.sortBy = sortBy;
+ }
+ },
+ status: getStatus,
+ },
+};
+</script>
+
+<style scoped>
+.cw-dashboard-students-wrapper >>> .cw-ribbon-nav {
+ min-width: 24px;
+ padding: 0 1em;
+ height: 24px;
+ margin-top: 2px;
+}
+th {
+ cursor: pointer;
+}
+th:is(:first-child,:last-child) {
+ cursor: not-allowed;
+}
+</style>
diff --git a/resources/vue/components/courseware/CoursewareDashboardTasks.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
index 6de9c13..9c9e298 100644
--- a/resources/vue/components/courseware/CoursewareDashboardTasks.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
@@ -102,11 +102,11 @@
</div>
</template>
<script>
-import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
-import StudipIcon from '../StudipIcon.vue';
-import StudipActionMenu from '../StudipActionMenu.vue';
-import StudipDialog from '../StudipDialog.vue';
-import taskHelperMixin from '../../mixins/courseware/task-helper.js';
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import StudipIcon from '../../StudipIcon.vue';
+import StudipActionMenu from '../../StudipActionMenu.vue';
+import StudipDialog from '../../StudipDialog.vue';
+import taskHelperMixin from '../../../mixins/courseware/task-helper.js';
import { mapActions, mapGetters } from 'vuex';
export default {
diff --git a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
index 79c8cac..e88bd04 100644
--- a/resources/vue/components/courseware/CoursewareTasksDialogDistribute.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
@@ -27,10 +27,10 @@
:key="'label-' + unit.id"
:for="'cw-task-dist-source-unit' + unit.id"
>
- <div class="icon"><studip-icon shape="courseware" size="32" /></div>
+ <div class="icon"><studip-icon shape="courseware" :size="32" /></div>
<div class="text">{{ unit.element.attributes.title }}</div>
- <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
- <studip-icon shape="check-circle" size="24" class="check" />
+ <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+ <studip-icon shape="check-circle" :size="24" class="check" />
</label>
</template>
</fieldset>
@@ -63,9 +63,14 @@
<input type="text" v-model="taskTitle" required />
</label>
<label>
+ <span>{{ $gettext('Startdatum') }}</span>
+ <span aria-hidden="true" class="wizard-required">*</span>
+ <input type="date" v-model="startDate" required />
+ </label>
+ <label>
<span>{{ $gettext('Abgabefrist') }}</span>
<span aria-hidden="true" class="wizard-required">*</span>
- <input type="date" v-model="submissionDate" />
+ <input type="date" v-model="endDate" :min="startDate" required />
</label>
<label>
{{ $gettext('Inhalte ergänzen') }}
@@ -99,10 +104,10 @@
:key="'label-' + unit.id"
:for="'cw-task-dist-target-unit' + unit.id"
>
- <div class="icon"><studip-icon shape="courseware" size="32" /></div>
+ <div class="icon"><studip-icon shape="courseware" :size="32" /></div>
<div class="text">{{ unit.element.attributes.title }}</div>
- <studip-icon shape="radiobutton-unchecked" size="24" class="unchecked" />
- <studip-icon shape="check-circle" size="24" class="check" />
+ <studip-icon shape="radiobutton-unchecked" :size="24" class="unchecked" />
+ <studip-icon shape="check-circle" :size="24" class="check" />
</label>
</template>
</fieldset>
@@ -237,12 +242,15 @@
</template>
<script>
-import CoursewareCompanionBox from './layouts/CoursewareCompanionBox.vue';
-import CoursewareStructuralElementSelector from './structural-element/CoursewareStructuralElementSelector.vue';
-import StudipWizardDialog from '../StudipWizardDialog.vue';
+import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareStructuralElementSelector from '../structural-element/CoursewareStructuralElementSelector.vue';
+import StudipWizardDialog from '../../StudipWizardDialog.vue';
import { mapActions, mapGetters } from 'vuex';
+const dateString = (date) =>
+ `${date.getFullYear()}-${('' + (date.getMonth() + 1)).padStart(2, '0')}-${('' + date.getDate()).padStart(2, '0')}`;
+
export default {
name: 'courseware-tasks-dialog-distribute',
components: {
@@ -316,7 +324,8 @@ export default {
],
selectedSourceUnit: null,
taskTitle: '',
- submissionDate: '',
+ startDate: dateString(new Date()),
+ endDate: '',
solverMayAddBlocks: true,
selectedTask: null,
selectedTargetUnit: null,
@@ -488,7 +497,7 @@ export default {
},
methods: {
...mapActions({
- setShowTasksDistributeDialog: 'setShowTasksDistributeDialog',
+ setShowTasksDistributeDialog: 'tasks/setShowTasksDistributeDialog',
loadCourseUnits: 'loadCourseUnits',
loadUserUnits: 'loadUserUnits',
loadStructuralElement: 'courseware-structural-elements/loadById',
@@ -522,10 +531,21 @@ export default {
return;
}
this.distributing = true;
+ const startDate = new Date(this.startDate);
+ startDate.setHours(0);
+ startDate.setMinutes(0);
+ startDate.setSeconds(0);
+ startDate.setMilliseconds(0);
+ const endDate = new Date(this.endDate);
+ endDate.setHours(23);
+ endDate.setMinutes(59);
+ endDate.setSeconds(59);
+ endDate.setMilliseconds(999);
const taskGroup = {
attributes: {
title: this.taskTitle,
- 'submission-date': new Date(this.submissionDate).toISOString(),
+ 'start-date': startDate.toISOString(),
+ 'end-date': endDate.toISOString(),
'solver-may-add-blocks': this.solverMayAddBlocks,
},
relationships: {
@@ -560,7 +580,7 @@ export default {
this.$emit('newtask');
this.distributing = false;
this.setShowTasksDistributeDialog(false);
-
+
},
validateSolvers() {
if (
@@ -575,7 +595,7 @@ export default {
return this.wizardSlots[5].valid;
},
validateTaskSettings() {
- if (this.taskTitle !== '' && this.submissionDate !== '') {
+ if (this.taskTitle !== '' && this.endDate !== '') {
this.wizardSlots[2].valid = true;
} else {
this.wizardSlots[2].valid = false;
@@ -651,7 +671,14 @@ export default {
taskTitle() {
this.validate();
},
- submissionDate() {
+ startDate() {
+ if (new Date(this.startDate) > new Date(this.endDate)) {
+ const endDate = new Date(this.startDate);
+ endDate.setDate(endDate.getDate() + 1);
+ this.endDate = dateString(endDate);
+ }
+ },
+ endDate() {
this.validate();
},
selectedAutors() {
diff --git a/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue b/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue
new file mode 100644
index 0000000..a07356d
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/EditFeedbackDialog.vue
@@ -0,0 +1,60 @@
+<template>
+ <studip-dialog
+ :title="$gettext('Feedback zur Aufgabe ändern')"
+ :confirmText="$gettext('Speichern')"
+ confirmClass="accept"
+ :closeText="$gettext('Schließen')"
+ closeClass="cancel"
+ height="420"
+ @close="$emit('close')"
+ @confirm="update"
+ >
+ <template #dialogContent>
+ <CompanionBox
+ v-if="localContent === ''"
+ mood="pointing"
+ :msgCompanion="
+ $gettext('Sie haben kein Feedback geschrieben, beim Speichern wird dieses Feedback gelöscht!')
+ "
+ />
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Feedback') }}
+ <textarea v-model="localContent" />
+ </label>
+ </form>
+ </template>
+ </studip-dialog>
+</template>
+
+<script>
+import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
+
+export default {
+ props: ['content'],
+ components: {
+ CompanionBox,
+ },
+ data: () => ({
+ localContent: '',
+ }),
+ methods: {
+ resetLocalVars() {
+ this.localContent = this.content;
+ },
+ update() {
+ this.$emit('update', { content: this.localContent });
+ },
+ },
+ mounted() {
+ this.resetLocalVars();
+ },
+ watch: {
+ content(newValue) {
+ if (newValue !== this.localContent) {
+ this.resetLocalVars();
+ }
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/TasksApp.vue b/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue
index 8a406d8..5701580 100644
--- a/resources/vue/components/courseware/TasksApp.vue
+++ b/resources/vue/components/courseware/tasks/PagesTaskGroupsIndex.vue
@@ -1,21 +1,21 @@
<template>
<div class="cw-tasks-wrapper">
<div class="cw-tasks-list">
- <courseware-dashboard-students v-if="userIsTeacher" />
- <courseware-dashboard-tasks v-else />
+ <CoursewareDashboardStudents v-if="userIsTeacher" />
+ <CoursewareDashboardTasks v-else />
</div>
<MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
- <courseware-tasks-action-widget />
+ <CoursewareTasksActionWidget />
</MountingPortal>
<courseware-companion-overlay />
</div>
</template>
<script>
-import CoursewareTasksActionWidget from './widgets/CoursewareTasksActionWidget.vue';
+import CoursewareTasksActionWidget from '../widgets/CoursewareTasksActionWidget.vue';
import CoursewareDashboardTasks from './CoursewareDashboardTasks.vue';
import CoursewareDashboardStudents from './CoursewareDashboardStudents.vue';
-import CoursewareCompanionOverlay from './layouts/CoursewareCompanionOverlay.vue';
+import CoursewareCompanionOverlay from '../layouts/CoursewareCompanionOverlay.vue';
import { mapGetters } from 'vuex';
export default {
diff --git a/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue b/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue
new file mode 100644
index 0000000..e17d18e
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/PagesTaskGroupsShow.vue
@@ -0,0 +1,224 @@
+<template>
+ <div class="cw-tasks-wrapper">
+ <MountingPortal mountTo="#courseware-action-widget" name="sidebar-actions" v-if="userIsTeacher">
+ <CoursewareTasksActionWidget :taskGroup="taskGroup" />
+ </MountingPortal>
+
+ <div v-if="taskGroup" class="cw-tasks-list">
+ <CoursewareRibbon :isContentBar="true" :showToolbarButton="false">
+ <template #buttons>
+ <router-link :to="{ name: 'task-groups-index' }">
+ <StudipIcon shape="category-task" :size="24" />
+ </router-link>
+ </template>
+ <template #breadcrumbList>
+ <li>
+ <router-link :to="{ name: 'task-groups-index' }">
+ {{ $gettext('Aufgaben') }}
+ </router-link>
+ </li>
+ <li>{{ taskGroup.attributes['title'] }}</li>
+ </template>
+ </CoursewareRibbon>
+
+ <TaskGroup
+ :taskGroup="taskGroup"
+ :tasks="tasksByGroup[taskGroup.id]"
+ @add-feedback="onShowAddFeedback"
+ @edit-feedback="onShowEditFeedback"
+ @solve-renewal="onShowSolveRenewal"
+ />
+ </div>
+ <CompanionBox
+ v-else-if="!tasksLoading"
+ :msgCompanion="$gettext('Diese Courseware-Aufgabe konnte nicht gefunden werden.')"
+ />
+
+ <AddFeedbackDialog
+ v-if="showAddFeedbackDialog"
+ :content="currentDialogFeedback.attributes.content"
+ @create="createFeedback"
+ @close="closeDialogs"
+ />
+
+ <EditFeedbackDialog
+ v-if="showEditFeedbackDialog"
+ :content="currentDialogFeedback.attributes.content"
+ @update="updateFeedback"
+ @close="closeDialogs"
+ />
+
+ <RenewalDialog
+ v-if="renewalTask"
+ :renewalDate="renewalDate"
+ :renewalState="renewalTask.attributes.renewal"
+ @update="updateRenewal"
+ @close="closeDialogs"
+ />
+
+ <TaskGroupsAddSolversDialog v-if="showTaskGroupsAddSolversDialog" :taskGroup="taskGroup" @newtask="reloadTasks" />
+ <TaskGroupsDeleteDialog v-if="showTaskGroupsDeleteDialog" :taskGroup="taskGroup" />
+ <TaskGroupsModifyDeadlineDialog v-if="showTaskGroupsModifyDeadlineDialog" :taskGroup="taskGroup" />
+ <CoursewareTasksDialogDistribute v-if="showTasksDistributeDialog" @newtask="reloadTasks" />
+ </div>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import AddFeedbackDialog from './AddFeedbackDialog.vue';
+import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import CoursewareRibbon from '../structural-element/CoursewareRibbon.vue';
+import CoursewareTasksActionWidget from '../widgets/CoursewareTasksActionWidget.vue';
+import CoursewareTasksDialogDistribute from './CoursewareTasksDialogDistribute.vue';
+import EditFeedbackDialog from './EditFeedbackDialog.vue';
+import RenewalDialog from './RenewalDialog.vue';
+import TaskGroup from './TaskGroup.vue';
+import TaskGroupsAddSolversDialog from './TaskGroupsAddSolversDialog.vue';
+import TaskGroupsDeleteDialog from './TaskGroupsDeleteDialog.vue';
+import TaskGroupsModifyDeadlineDialog from './TaskGroupsModifyDeadlineDialog.vue';
+
+export default {
+ components: {
+ AddFeedbackDialog,
+ CompanionBox,
+ CoursewareRibbon,
+ CoursewareTasksActionWidget,
+ CoursewareTasksDialogDistribute,
+ EditFeedbackDialog,
+ RenewalDialog,
+ TaskGroup,
+ TaskGroupsAddSolversDialog,
+ TaskGroupsDeleteDialog,
+ TaskGroupsModifyDeadlineDialog,
+ },
+ props: ['id'],
+ data() {
+ return {
+ currentDialogFeedback: {},
+ renewalTask: null,
+ showAddFeedbackDialog: false,
+ showEditFeedbackDialog: false,
+ };
+ },
+ computed: {
+ ...mapGetters({
+ context: 'context',
+ getTaskGroup: 'courseware-task-groups/byId',
+ showTaskGroupsAddSolversDialog: 'tasks/showTaskGroupsAddSolversDialog',
+ showTaskGroupsDeleteDialog: 'tasks/showTaskGroupsDeleteDialog',
+ showTaskGroupsModifyDeadlineDialog: 'tasks/showTaskGroupsModifyDeadlineDialog',
+ showTasksDistributeDialog: 'tasks/showTasksDistributeDialog',
+ tasksByCid: 'tasks/tasksByCid',
+ tasksLoading: 'courseware-tasks/isLoading',
+ userIsTeacher: 'userIsTeacher',
+ }),
+ renewalDate() {
+ return this.renewalTask ? new Date(this.renewalTask.attributes['renewal-date']) : new Date();
+ },
+ taskGroup() {
+ return this.getTaskGroup({ id: this.id });
+ },
+ tasksByGroup() {
+ return this.tasksByCid(this.context.id).reduce((memo, task) => {
+ const key = task.relationships['task-group'].data.id;
+ (memo[key] || (memo[key] = [])).push(task);
+
+ return memo;
+ }, {});
+ },
+ },
+ methods: {
+ ...mapActions({
+ companionError: 'companionError',
+ companionSuccess: 'companionSuccess',
+ createTaskFeedback: 'createTaskFeedback',
+ deleteTaskFeedback: 'deleteTaskFeedback',
+ loadAllTasks: 'courseware-tasks/loadAll',
+ loadTaskGroup: 'tasks/loadTaskGroup',
+ updateTask: 'updateTask',
+ updateTaskFeedback: 'updateTaskFeedback',
+ }),
+ closeDialogs() {
+ this.showAddFeedbackDialog = false;
+ this.showEditFeedbackDialog = false;
+
+ this.currentDialogFeedback = {};
+ this.renewalTask = null;
+ },
+ createFeedback({ content }) {
+ if (content === '') {
+ this.companionError({
+ info: this.$gettext('Bitte schreiben Sie ein Feedback.'),
+ });
+ return false;
+ }
+ this.currentDialogFeedback.attributes.content = content;
+ this.createTaskFeedback({ taskFeedback: this.currentDialogFeedback });
+ this.closeDialogs();
+ },
+ onShowAddFeedback(task) {
+ this.currentDialogFeedback = {
+ attributes: { content: '' },
+ relationships: {
+ task: {
+ data: {
+ id: task.id,
+ type: task.type,
+ },
+ },
+ },
+ };
+ this.showAddFeedbackDialog = true;
+ },
+ onShowEditFeedback(feedback) {
+ this.currentDialogFeedback = _.cloneDeep(feedback);
+ this.showEditFeedbackDialog = true;
+ },
+ onShowSolveRenewal(task) {
+ this.renewalTask = _.cloneDeep(task);
+ this.renewalTask.attributes['renewal-date'] = new Date().toISOString();
+ },
+ reloadTasks() {
+ this.loadAllTasks({
+ options: {
+ 'filter[cid]': this.context.id,
+ include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer',
+ },
+ });
+ },
+ updateRenewal({ state, date }) {
+ const attributes = { renewal: state };
+ if (date) {
+ attributes['renewal-date'] = date.toISOString();
+ }
+
+ this.updateTask({ attributes, taskId: this.renewalTask.id });
+ this.closeDialogs();
+ },
+ async updateFeedback({ content }) {
+ if (content === '') {
+ await this.deleteTaskFeedback({ taskFeedbackId: this.currentDialogFeedback.id });
+ this.companionSuccess({ info: this.$gettext('Feedback wurde gelöscht.') });
+ } else {
+ await this.updateTaskFeedback({
+ attributes: { content },
+ taskFeedbackId: this.currentDialogFeedback.id,
+ });
+ this.companionSuccess({
+ info: this.$gettext('Feedback wurde gespeichert.'),
+ });
+ }
+ this.closeDialogs();
+ },
+ },
+};
+</script>
+
+<style scoped>
+.cw-tasks-wrapper >>> .cw-ribbon-nav {
+ min-width: 24px;
+ padding: 0 1em;
+ height: 24px;
+ margin-top: 2px;
+}
+</style>
diff --git a/resources/vue/components/courseware/tasks/RenewalDialog.vue b/resources/vue/components/courseware/tasks/RenewalDialog.vue
new file mode 100644
index 0000000..f08719e
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/RenewalDialog.vue
@@ -0,0 +1,79 @@
+<template>
+ <studip-dialog
+ :title="$gettext('Verlängerungsanfrage bearbeiten')"
+ :confirmText="$gettext('Speichern')"
+ confirmClass="accept"
+ :closeText="$gettext('Schließen')"
+ closeClass="cancel"
+ height="350"
+ @close="$emit('close')"
+ @confirm="updateRenewal"
+ >
+ <template #dialogContent>
+ <form class="default" @submit.prevent="">
+ <label>
+ {{ $gettext('Fristverlängerung') }}
+ <select v-model="state">
+ <option value="declined">
+ {{ $gettext('ablehnen') }}
+ </option>
+ <option value="granted">
+ {{ $gettext('gewähren') }}
+ </option>
+ </select>
+ </label>
+ <label v-if="state === 'granted'">
+ {{ $gettext('neue Frist') }}
+ <DateInput v-model="date" class="size-l" />
+ </label>
+ </form>
+ </template>
+ </studip-dialog>
+</template>
+
+<script>
+import DateInput from '../layouts/CoursewareDateInput.vue';
+export default {
+ props: ['renewalDate', 'renewalState'],
+ components: {
+ DateInput,
+ },
+ data: () => ({
+ date: null,
+ state: null,
+ }),
+ methods: {
+ resetLocalVars() {
+ this.date = this.renewalDate ?? null;
+ this.state = this.renewalState;
+ },
+ updateRenewal() {
+ const date = new Date(this.date);
+ date.setHours(23);
+ date.setMinutes(59);
+ date.setSeconds(59);
+ date.setMilliseconds(999);
+
+ this.$emit('update', {
+ state: this.state,
+ date: this.state === 'granted' ? date || Date.now() : null,
+ });
+ },
+ },
+ mounted() {
+ this.resetLocalVars();
+ },
+ watch: {
+ renewalDate(newValue) {
+ if (newValue !== this.date) {
+ this.resetLocalVars();
+ }
+ },
+ renewalState(newValue) {
+ if (newValue !== this.state) {
+ this.resetLocalVars();
+ }
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/TaskGroup.vue b/resources/vue/components/courseware/tasks/TaskGroup.vue
new file mode 100644
index 0000000..62449f1
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/TaskGroup.vue
@@ -0,0 +1,84 @@
+<template>
+ <div>
+ <CompanionBox :msgCompanion="statusMessage">
+ <template #companionActions>
+ <span>
+ {{ $gettext('Bearbeitungszeit') }}
+ <StudipDate :date="startDate" /> - <StudipDate :date="endDate" />
+ </span>
+ </template>
+ </CompanionBox>
+
+ <section v-if="tasks.length > 0">
+ <table class="default">
+ <caption>
+ {{ $gettext('Verteilte Aufgaben') }}
+ </caption>
+ <thead>
+ <tr>
+ <th>{{ $gettext('Status') }}</th>
+ <th>{{ $gettext('Teilnehmende/Gruppen') }}</th>
+ <th class="responsive-hidden">{{ $gettext('Seite') }}</th>
+ <th>{{ $gettext('bearbeitet') }}</th>
+ <th>{{ $gettext('Abgabefrist') }}</th>
+ <th>{{ $gettext('Abgabe') }}</th>
+ <th class="responsive-hidden renewal">{{ $gettext('Verlängerungsanfrage') }}</th>
+ <th class="responsive-hidden feedback">{{ $gettext('Feedback') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <TaskItem
+ v-for="task in tasks"
+ :task="task"
+ :taskGroup="taskGroup"
+ :key="task.id"
+ @add-feedback="(task) => $emit('add-feedback', task)"
+ @edit-feedback="(feedback) => $emit('edit-feedback', feedback)"
+ @solve-renewal="(task) => $emit('solve-renewal', task)"
+ />
+ </tbody>
+ </table>
+ </section>
+ <div v-else>
+ <CompanionBox mood="pointing" :msgCompanion="$gettext('Diese Aufgabe wurde an niemanden verteilt.')" />
+ </div>
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex';
+import CompanionBox from '../layouts/CoursewareCompanionBox.vue';
+import StudipDate from '../../StudipDate.vue';
+import TaskItem from './TaskGroupTaskItem.vue';
+import { getStatus } from './task-groups-helper.js';
+
+export default {
+ components: { CompanionBox, StudipDate, TaskItem },
+ props: ['taskGroup', 'tasks'],
+ computed: {
+ ...mapGetters({
+ coursewareContext: 'context',
+ }),
+ actionMenuContext() {
+ return this.$gettextInterpolate(this.$gettext('Courseware-Aufgabe "%{ taskGroup }"'), {
+ taskGroup: this.taskGroup.attributes.title,
+ });
+ },
+ endDate() {
+ return new Date(this.taskGroup.attributes['end-date']);
+ },
+ isAfter() {
+ return new Date() > this.endDate;
+ },
+ startDate() {
+ return new Date(this.taskGroup.attributes['start-date']);
+ },
+ status() {
+ return getStatus(this.taskGroup);
+ },
+ statusMessage() {
+ return this.status.description;
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue b/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue
new file mode 100644
index 0000000..b684f10
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/TaskGroupTaskItem.vue
@@ -0,0 +1,118 @@
+<template>
+ <tr>
+ <td>
+ <studip-icon
+ v-if="status.shape !== undefined"
+ :shape="status.shape"
+ :role="status.role"
+ :title="status.description"
+ aria-hidden="true"
+ />
+ <span class="sr-only">{{ status.description }}</span>
+ </td>
+ <td>
+ <span v-if="user">
+ <studip-icon shape="person2" role="info" aria-hidden="true" :title="$gettext('Teilnehmende Person')" />
+ <span class="sr-only">{{ $gettext('Teilnehmende Person') }}</span>
+ {{ user.attributes['formatted-name'] }}
+ </span>
+ <span v-if="group">
+ <studip-icon shape="group2" role="info" aria-hidden="true" :title="$gettext('Gruppe')" />
+ <span class="sr-only">{{ $gettext('Gruppe') }}</span>
+ {{ group.attributes['name'] }}
+ </span>
+ </td>
+ <td class="responsive-hidden">
+ <a v-if="task.attributes.submitted" :href="getLinkToElement(element)">
+ {{ element.attributes.title }}
+ </a>
+ <span v-else>{{ element.attributes.title }}</span>
+ </td>
+ <td>{{ task.attributes?.progress?.toFixed(2) || '-.--' }}%</td>
+ <td>{{ getReadableDate(task.attributes['submission-date']) }}</td>
+ <td>
+ <studip-icon v-if="task.attributes.submitted" shape="accept" role="status-green" />
+ </td>
+ <td class="responsive-hidden">
+ <button v-show="task.attributes.renewal === 'pending'" class="button" @click="$emit('solve-renewal', task)">
+ {{ $gettext('Anfrage bearbeiten') }}
+ </button>
+ <span v-show="task.attributes.renewal === 'declined'">
+ <studip-icon shape="decline" role="status-red" />
+ {{ $gettext('Anfrage abgelehnt') }}
+ </span>
+ <span v-show="task.attributes.renewal === 'granted'">
+ {{ $gettext('verlängert bis') }}:
+ {{ getReadableDate(task.attributes['renewal-date']) }}
+ </span>
+ <studip-icon
+ v-if="task.attributes.renewal === 'declined' || task.attributes.renewal === 'granted'"
+ :title="$gettext('Anfrage bearbeiten')"
+ class="edit"
+ shape="edit"
+ @click="$emit('solve-renewal', task)"
+ />
+ </td>
+ <td class="responsive-hidden">
+ <span
+ v-if="feedback"
+ :title="
+ $gettextInterpolate($gettext('Feedback geschrieben am: %{ date }'), {
+ date: getReadableDate(feedback.attributes['chdate']),
+ })
+ "
+ >
+ <studip-icon shape="accept" role="status-green" />
+ {{ $gettext('Feedback gegeben') }}
+ <studip-icon
+ :title="$gettext('Feedback bearbeiten')"
+ class="edit"
+ shape="edit"
+ @click="$emit('edit-feedback', feedback)"
+ />
+ </span>
+
+ <button v-show="!feedback && task.attributes.submitted" class="button" @click="$emit('add-feedback', task)">
+ {{ $gettext('Feedback geben') }}
+ </button>
+ </td>
+ </tr>
+</template>
+<script>
+import taskHelper from '../../../mixins/courseware/task-helper.js';
+import { mapGetters } from 'vuex';
+
+export default {
+ mixins: [taskHelper],
+ props: ['task', 'taskGroup'],
+ computed: {
+ ...mapGetters({
+ elementById: 'courseware-structural-elements/byId',
+ feedbackById: 'courseware-task-feedback/byId',
+ statusGroupById: 'status-groups/byId',
+ userById: 'users/byId',
+ }),
+ element() {
+ return this.elementById({ id: this.task.relationships['structural-element'].data.id });
+ },
+ feedback() {
+ const id = this.task.relationships['task-feedback'].data?.id;
+ return id ? this.feedbackById({ id }) : null;
+ },
+ group() {
+ const { id, type } = this.solver;
+ return type === 'status-groups' ? this.statusGroupById({ id }) : null;
+ },
+ solver() {
+ return this.task.relationships.solver.data;
+ },
+ status() {
+ return this.getStatus(this.task);
+ },
+ user() {
+ const { id, type } = this.solver;
+ return type === 'users' ? this.userById({ id }) : null;
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
new file mode 100644
index 0000000..a843341
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
@@ -0,0 +1,224 @@
+<template>
+ <studip-dialog
+ :title="$gettext('Teilnehmende hinzufügen')"
+ :confirmText="$gettext('Hinzufügen')"
+ confirmClass="accept"
+ :confirmDisabled="!taskSolverType"
+ :closeText="$gettext('Abbrechen')"
+ closeClass="cancel"
+ @close="onClose"
+ @confirm="onConfirm"
+ width="700"
+ >
+ <template #dialogContent>
+ <form class="default">
+ <label>
+ {{ $gettext('Verteilen an') }}
+ <select v-model="taskSolverType">
+ <option value="users">{{ $gettext('Studierende') }}</option>
+ <option value="status-groups">{{ $gettext('Gruppen') }}</option>
+ </select>
+ </label>
+
+ <template v-if="taskSolverType === 'users'">
+ <CoursewareCompanion
+ v-show="autor_members.length === 0"
+ :msgCompanion="$gettext('Es wurden keine Studierenden in dieser Veranstaltung gefunden.')"
+ mood="pointing"
+ />
+ <table v-show="autor_members.length > 0" class="default">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ $gettext('Name') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="user in autor_members" :key="user.user_id">
+ <td>
+ <input
+ type="checkbox"
+ v-model="selectedAutors"
+ :disabled="isSolver(user.user_id)"
+ :value="user.user_id"
+ :aria-label="
+ $gettextInterpolate($gettext('%{userName} auswählen'), {
+ userName: user.formattedname,
+ })
+ "
+ />
+ </td>
+ <td>{{ user.formattedname }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </template>
+ <template v-if="taskSolverType === 'status-groups'">
+ <CoursewareCompanion
+ v-show="groups.length === 0"
+ :msgCompanion="$gettext('Es wurden keine Gruppen in dieser Veranstaltung gefunden.')"
+ mood="pointing"
+ />
+ <table v-show="groups.length > 0" class="default">
+ <thead>
+ <tr>
+ <th></th>
+ <th>{{ $gettext('Gruppenname') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="group in groups" :key="group.id">
+ <td>
+ <input
+ type="checkbox"
+ v-model="selectedGroups"
+ :disabled="isSolver(group.id)"
+ :value="group.id"
+ :aria-label="
+ $gettextInterpolate($gettext('%{groupName} auswählen'), {
+ groupName: group.name,
+ })
+ "
+ />
+ </td>
+ <td>{{ group.name }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </template>
+ </form>
+ </template>
+ </studip-dialog>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import CoursewareCompanion from '../layouts/CoursewareCompanionBox.vue';
+
+export default {
+ props: ['taskGroup'],
+ components: {
+ CoursewareCompanion,
+ },
+ data: () => ({
+ selectedAutors: [],
+ selectedGroups: [],
+ storing: false,
+ taskSolverType: null,
+ }),
+ computed: {
+ ...mapGetters({
+ context: 'context',
+ relatedCourseMemberships: 'course-memberships/related',
+ relatedCourseStatusGroups: 'status-groups/related',
+ relatedUser: 'users/related',
+ tasksByCid: 'tasks/tasksByCid',
+ }),
+ autor_members() {
+ return Object.keys(this.users).length === 0 && this.users.constructor === Object
+ ? []
+ : this.users.filter(({ perm }) => perm === 'autor').map((obj) => ({ ...obj, active: false }));
+ },
+ groups() {
+ return (
+ this.relatedCourseStatusGroups({
+ parent: { type: 'courses', id: this.context.id },
+ relationship: 'status-groups',
+ })?.map(({ id, attributes: { name } }) => ({ id, name })) ?? []
+ );
+ },
+ solversById() {
+ return new Map(this.solvers.map(({ id, type }) => [id, { id, type }]));
+ },
+ solvers() {
+ return this.tasks.map((task) => task.relationships.solver.data);
+ },
+ tasks() {
+ return this.tasksByCid(this.context.id).filter(
+ (task) => task.relationships['task-group'].data.id === this.taskGroup.id
+ );
+ },
+ users() {
+ const memberships = this.relatedCourseMemberships({
+ parent: { type: 'courses', id: this.context.id },
+ relationship: 'memberships',
+ });
+
+ return (
+ memberships?.map(({ type, id, attributes: { permission } }) => {
+ const member = this.relatedUser({ parent: { type, id }, relationship: 'user' });
+
+ return {
+ user_id: member.id,
+ formattedname: member.attributes['formatted-name'],
+ username: member.attributes['username'],
+ perm: permission,
+ };
+ }) ?? []
+ );
+ },
+ },
+ methods: {
+ ...mapActions({
+ addSolversToTaskGroup: 'tasks/addSolversToTaskGroup',
+ loadCourseMemberships: 'course-memberships/loadRelated',
+ loadCourseStatusGroups: 'status-groups/loadRelated',
+ setShowDialog: 'tasks/setShowTaskGroupsAddSolversDialog',
+ }),
+ isSolver(id) {
+ return !!this.solvers.find((solver) => solver.id === id);
+ },
+ onClose() {
+ this.setShowDialog(false);
+ },
+ onConfirm() {
+ if (!this.taskSolverType || this.storing) {
+ return;
+ }
+ this.storing = true;
+
+ const solvers = this[this.taskSolverType === 'users' ? 'selectedAutors' : 'selectedGroups'];
+ const ids = solvers.filter((id) => !this.solversById.has(id));
+ this.addSolversToTaskGroup({
+ taskGroup: this.taskGroup,
+ solvers: ids.map((id) => ({ id, type: this.taskSolverType })),
+ })
+ .then(() => {
+ this.$emit('newtask');
+ this.onClose();
+ })
+ .finally(() => (this.storing = false));
+ },
+ resetLocalVars() {
+ this.selectedAutors = this.solvers.filter(({ type }) => type === 'users').map(({ id }) => id);
+ this.selectedGroups = this.solvers.filter(({ type }) => type === 'status-groups').map(({ id }) => id);
+ this.taskSolverType = this.selectedAutors.length
+ ? 'users'
+ : this.selectedGroups.length
+ ? 'status-groups'
+ : null;
+ },
+ },
+ mounted() {
+ this.resetLocalVars();
+
+ const parent = { type: 'courses', id: this.context.id };
+ this.loadCourseMemberships({
+ parent,
+ relationship: 'memberships',
+ options: {
+ include: 'user',
+ 'page[offset]': 0,
+ 'page[limit]': 10000,
+ 'filter[permission]': 'autor',
+ },
+ });
+ this.loadCourseStatusGroups({ parent, relationship: 'status-groups' });
+ },
+ watch: {
+ taskGroup() {
+ this.resetLocalVars();
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsDeleteDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsDeleteDialog.vue
new file mode 100644
index 0000000..b1a151d
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/TaskGroupsDeleteDialog.vue
@@ -0,0 +1,33 @@
+<template>
+ <studip-dialog
+ :title="$gettext('Aufgabe löschen')"
+ :question="$gettext('Möchten Sie die Aufgabe wirklich löschen?')"
+ height="200"
+ @close="onClose"
+ @confirm="onConfirm"
+ >
+ </studip-dialog>
+</template>
+
+<script>
+import { mapActions } from 'vuex';
+
+export default {
+ props: ['taskGroup'],
+ methods: {
+ ...mapActions({
+ deleteTaskGroup: 'courseware-task-groups/delete',
+ setShowTaskGroupsDeleteDialog: 'tasks/setShowTaskGroupsDeleteDialog'
+ }),
+ onClose() {
+ this.setShowTaskGroupsDeleteDialog(false);
+ },
+ onConfirm() {
+ this.deleteTaskGroup(this.taskGroup).then(() => {
+ this.onClose();
+ this.$router.push({ name: 'task-groups-index' });
+ });
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue
new file mode 100644
index 0000000..39198af
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/TaskGroupsModifyDeadlineDialog.vue
@@ -0,0 +1,117 @@
+<template>
+ <studip-dialog
+ :title="$gettext('Bearbeitungszeit verlängern')"
+ :confirmText="$gettext('Verlängern')"
+ confirmClass="accept"
+ :closeText="$gettext('Abbrechen')"
+ closeClass="cancel"
+ @close="onClose"
+ @confirm="onConfirm"
+ >
+ <template #dialogContent>
+ <form class="default">
+ <p>
+ {{ $gettext('Aktuelle Bearbeitungszeit:') }} <StudipDate :date="startDate" /> - <StudipDate
+ :date="endDate"
+ />
+ ({{ $gettextInterpolate($gettext('%{ count } Tage'), { count: oldDuration }) }})
+ </p>
+ <div class="formpart">
+ <label class="studiprequired">
+ <span class="textlabel">{{ $gettext('Bearbeitungszeit verlängern bis zum') }}</span>
+ <span class="asterisk" :title="$gettext('Dies ist ein Pflichtfeld')" aria-hidden="true">*</span>
+ <input
+ :id="`task-groups-${uid}`"
+ name="end-date"
+ type="date"
+ v-model="localEndDate"
+ :min="endDateString"
+ class="size-l"
+ required
+ />
+ </label>
+ </div>
+ <p>
+ {{ $gettext('Verlängerte Bearbeitungszeit:') }} <StudipDate :date="startDate" /> - <StudipDate
+ :date="newEndDate"
+ />
+ ({{ $gettextInterpolate($gettext('%{ count } Tage'), { count: newDuration }) }})
+ </p>
+ </form>
+ </template>
+ </studip-dialog>
+</template>
+
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import StudipDate from '../../StudipDate.vue';
+
+const midnight = (_date) => {
+ const date = new Date(_date);
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ return date;
+};
+
+const dateString = (date) =>
+ `${date.getFullYear()}-${('' + (date.getMonth() + 1)).padStart(2, '0')}-${('' + date.getDate()).padStart(2, '0')}`;
+
+let nextUid = 0;
+
+export default {
+ props: ['taskGroup'],
+ components: {
+ StudipDate,
+ },
+ data: () => ({ localEndDate: null, uid: nextUid++ }),
+ computed: {
+ endDate() {
+ return midnight(this.taskGroup?.attributes?.['end-date'] ?? new Date());
+ },
+ endDateString() {
+ return dateString(this.endDate);
+ },
+ newDuration() {
+ return this.localEndDate
+ ? Math.floor((midnight(this.localEndDate) - this.startDate) / (1000 * 60 * 60 * 24))
+ : 0;
+ },
+ newEndDate() {
+ return this.localEndDate ? midnight(this.localEndDate) : this.endDate;
+ },
+ oldDuration() {
+ return Math.floor((this.endDate - this.startDate) / (1000 * 60 * 60 * 24));
+ },
+ startDate() {
+ return midnight(this.taskGroup.attributes['start-date']);
+ },
+ },
+ methods: {
+ ...mapActions({
+ modifyDeadline: 'tasks/modifyDeadlineOfTaskGroup',
+ setShowDialog: 'tasks/setShowTaskGroupsModifyDeadlineDialog',
+ }),
+ onClose() {
+ this.setShowDialog(false);
+ },
+ onConfirm() {
+ const endDate = midnight(this.localEndDate);
+ this.modifyDeadline({ taskGroup: this.taskGroup, endDate });
+ this.onClose();
+ },
+ resetLocalVars() {
+ this.localEndDate = dateString(this.endDate ?? new Date());
+ },
+ },
+ mounted() {
+ this.resetLocalVars();
+ },
+ watch: {
+ taskGroup() {
+ this.resetLocalVars();
+ },
+ },
+};
+</script>
diff --git a/resources/vue/components/courseware/tasks/task-groups-helper.js b/resources/vue/components/courseware/tasks/task-groups-helper.js
new file mode 100644
index 0000000..8a9e469
--- /dev/null
+++ b/resources/vue/components/courseware/tasks/task-groups-helper.js
@@ -0,0 +1,31 @@
+import { $gettext } from '../../../../assets/javascripts/lib/gettext';
+
+export function getStatus(taskGroup) {
+ const now = new Date();
+ const startDate = new Date(taskGroup.attributes['start-date']);
+ const endDate = new Date(taskGroup.attributes['end-date']);
+
+ if (startDate <= now && now <= endDate) {
+ return {
+ shape: 'span-3quarter',
+ role: 'status-green',
+ description: $gettext('Die Bearbeitungszeit hat begonnen.'),
+ };
+ }
+
+ if (now < startDate) {
+ return {
+ shape: 'span-empty',
+ role: 'status-yellow',
+ description: $gettext('Die Bearbeitungszeit hat noch nicht begonnen.'),
+ };
+ }
+
+ if (endDate < now) {
+ return {
+ shape: 'span-full',
+ role: 'status-red',
+ description: $gettext('Die Bearbeitungszeit ist beendet.'),
+ };
+ }
+}
diff --git a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
index c2f54e9..cf37c6f 100644
--- a/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
+++ b/resources/vue/components/courseware/widgets/CoursewareTasksActionWidget.vue
@@ -2,7 +2,24 @@
<sidebar-widget id="courseware-action-widget" :title="$gettext('Aktionen')">
<template #content>
<ul class="widget-list widget-links cw-action-widget">
- <li class="cw-action-widget-add">
+ <template v-if="taskGroup">
+ <li v-if="isBeforeEndDate" class="cw-action-widget-task-groups-deadline">
+ <button @click="modifyDeadline(taskGroup)">
+ {{ $gettext('Bearbeitungszeit verlängern') }}
+ </button>
+ </li>
+ <li v-if="isBeforeEndDate" class="cw-action-widget-task-groups-add-solvers">
+ <button @click="addSolvers(taskGroup)">
+ {{ $gettext('Teilnehmende hinzufügen') }}
+ </button>
+ </li>
+ <li class="cw-action-widget-task-groups-delete">
+ <button @click="deleteTaskGroup(taskGroup)">
+ {{ $gettext('Aufgabe löschen') }}
+ </button>
+ </li>
+ </template>
+ <li v-else class="cw-action-widget-add">
<button @click="setShowTasksDistributeDialog(true)">
{{ $gettext('Aufgabe verteilen') }}
</button>
@@ -22,10 +39,34 @@ export default {
components: {
SidebarWidget,
},
+ props: ['taskGroup'],
+ computed: {
+ isBeforeEndDate() {
+ return this.taskGroup && new Date() < new Date(this.taskGroup.attributes['end-date']);
+ },
+ },
methods: {
...mapActions({
- setShowTasksDistributeDialog: 'setShowTasksDistributeDialog',
+ addSolvers: 'tasks/setShowTaskGroupsAddSolversDialog',
+ deleteTaskGroup: 'tasks/setShowTaskGroupsDeleteDialog',
+ modifyDeadline: 'tasks/setShowTaskGroupsModifyDeadlineDialog',
+ setShowTasksDistributeDialog: 'tasks/setShowTasksDistributeDialog',
}),
- }
+ },
+};
+</script>
+
+<style scoped>
+.cw-action-widget-task-groups-add-solvers {
+ background-image: url('../images/icons/blue/add.svg');
+ background-size: 16px;
+}
+.cw-action-widget-task-groups-deadline {
+ background-image: url('../images/icons/blue/date.svg');
+ background-size: 16px;
+}
+.cw-action-widget-task-groups-delete {
+ background-image: url('../images/icons/blue/trash.svg');
+ background-size: 16px;
}
-</script> \ No newline at end of file
+</style>
diff --git a/resources/vue/components/stock-images/colors.js b/resources/vue/components/stock-images/colors.js
index 4ba138c..910b143 100644
--- a/resources/vue/components/stock-images/colors.js
+++ b/resources/vue/components/stock-images/colors.js
@@ -1,4 +1,4 @@
-import { $gettext } from '@/assets/javascripts/lib/gettext.js';
+import { $gettext } from '@/assets/javascripts/lib/gettext';
const colors = [
{ name: $gettext('Schwarz'), hex: '#000000' },
diff --git a/resources/vue/components/stock-images/filters.js b/resources/vue/components/stock-images/filters.js
index 55cf726..42de27d 100644
--- a/resources/vue/components/stock-images/filters.js
+++ b/resources/vue/components/stock-images/filters.js
@@ -1,4 +1,4 @@
-import { $gettext } from '@/assets/javascripts/lib/gettext.js';
+import { $gettext } from '@/assets/javascripts/lib/gettext';
import { fromHex, rgbToCIELab, cie94 } from 'colorpare';
const SQUARE_DELTA = 1.1;
diff --git a/resources/vue/courseware-index-app.js b/resources/vue/courseware-index-app.js
index 191385f..59c0ebc 100644
--- a/resources/vue/courseware-index-app.js
+++ b/resources/vue/courseware-index-app.js
@@ -2,6 +2,7 @@ import CoursewareModule from './store/courseware/courseware.module';
import CoursewareStructureModule from './store/courseware/structure.module';
import FileChooserStore from './store/file-chooser.js';
import CoursewareStructuralElement from './components/courseware/structural-element/CoursewareStructuralElement.vue';
+import CoursewareTasksModule from './store/courseware/courseware-tasks.module';
import IndexApp from './components/courseware/IndexApp.vue';
import PluginManager from './components/courseware/plugin-manager.js';
import Vue from 'vue';
@@ -89,6 +90,7 @@ const mountApp = async (STUDIP, createApp, element) => {
courseware: CoursewareModule,
'courseware-structure': CoursewareStructureModule,
'file-chooser': FileChooserStore,
+ 'tasks': CoursewareTasksModule,
...mapResourceModules({
names: [
'courses',
diff --git a/resources/vue/courseware-tasks-app.js b/resources/vue/courseware-tasks-app.js
index 2f33246..9c01b71 100644
--- a/resources/vue/courseware-tasks-app.js
+++ b/resources/vue/courseware-tasks-app.js
@@ -1,5 +1,7 @@
-import TasksApp from './components/courseware/TasksApp.vue';
+import TaskGroupsIndex from './components/courseware/tasks/PagesTaskGroupsIndex.vue';
+import TaskGroupsShow from './components/courseware/tasks/PagesTaskGroupsShow.vue';
import { mapResourceModules } from '@elan-ev/reststate-vuex';
+import VueRouter, { RouterView } from 'vue-router';
import Vuex from 'vuex';
import CoursewareModule from './store/courseware/courseware.module';
import CoursewareTasksModule from './store/courseware/courseware-tasks.module';
@@ -17,6 +19,40 @@ const mountApp = async (STUDIP, createApp, element) => {
const httpClient = getHttpClient();
+ const routes = [
+ {
+ path: '/',
+ name: 'task-groups-index',
+ component: TaskGroupsIndex,
+ },
+ {
+ path: '/task-groups/:id',
+ name: 'task-groups-show',
+ component: TaskGroupsShow,
+ props: true,
+ },
+ ];
+
+ const base = new URL(
+ window.STUDIP.URLHelper.getURL(
+ 'dispatch.php/course/courseware/tasks',
+ { cid: STUDIP.URLHelper.parameters.cid },
+ true
+ )
+ );
+ const router = new VueRouter({
+ base: base.pathname,
+ mode: 'history',
+ routes,
+ });
+ router.beforeEach((to, from, next) => {
+ if ('cid' in to?.query) {
+ next();
+ } else {
+ next({ ...to, query: { ...to.query, cid: window.STUDIP.URLHelper.parameters.cid } });
+ }
+ });
+
const store = new Vuex.Store({
modules: {
courseware: CoursewareModule,
@@ -71,22 +107,18 @@ const mountApp = async (STUDIP, createApp, element) => {
}
store.dispatch('setUserId', STUDIP.USER_ID);
- await store.dispatch('users/loadById', {id: STUDIP.USER_ID});
+ await store.dispatch('users/loadById', { id: STUDIP.USER_ID });
store.dispatch('setHttpClient', httpClient);
store.dispatch('coursewareContext', {
id: entry_id,
type: entry_type,
});
await store.dispatch('loadTeacherStatus', STUDIP.USER_ID);
- store.dispatch('courseware-tasks/loadAll', {
- options: {
- 'filter[cid]': entry_id,
- include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer',
- },
- });
+ await store.dispatch('tasks/loadTasksOfCourse', { cid: entry_id });
const app = createApp({
- render: (h) => h(TasksApp),
+ render: (h) => h(RouterView),
+ router,
store,
});
diff --git a/resources/vue/mixins/courseware/task-helper.js b/resources/vue/mixins/courseware/task-helper.js
index 0bc694c..a0510f7 100644
--- a/resources/vue/mixins/courseware/task-helper.js
+++ b/resources/vue/mixins/courseware/task-helper.js
@@ -8,7 +8,7 @@ export default {
limit.setDate(now.getDate() + 3);
status.canSubmit = true;
- if (now < submissionDate) {
+ if (now <= submissionDate) {
status.shape = 'span-empty';
status.role = 'status-green';
status.description = this.$gettext('Aufgabe bereit');
@@ -20,7 +20,7 @@ export default {
status.description = this.$gettext('Aufgabe muss bald abgegeben werden');
}
- if (now >= submissionDate) {
+ if (now > submissionDate) {
status.canSubmit = false;
status.shape = 'span-full';
status.role = 'status-red';
@@ -34,7 +34,7 @@ export default {
status.description = this.$gettext('Aufgabe muss bald abgegeben werden');
}
- if (now >= renewalDate) {
+ if (now > renewalDate) {
status.canSubmit = false;
status.shape = 'span-full';
status.role = 'status-red';
diff --git a/resources/vue/store/AdminCoursesStore.js b/resources/vue/store/AdminCoursesStore.js
index 5b20e70..509239b 100644
--- a/resources/vue/store/AdminCoursesStore.js
+++ b/resources/vue/store/AdminCoursesStore.js
@@ -1,5 +1,5 @@
import Screenreader from '../../assets/javascripts/lib/screenreader.js';
-import { $gettext } from '../../assets/javascripts/lib/gettext.js';
+import { $gettext } from '../../assets/javascripts/lib/gettext';
export default {
namespaced: true,
diff --git a/resources/vue/store/courseware/courseware-tasks.module.js b/resources/vue/store/courseware/courseware-tasks.module.js
index fd5152d..0622452 100644
--- a/resources/vue/store/courseware/courseware-tasks.module.js
+++ b/resources/vue/store/courseware/courseware-tasks.module.js
@@ -1,5 +1,8 @@
const getDefaultState = () => {
return {
+ showTaskGroupsAddSolversDialog: false,
+ showTaskGroupsDeleteDialog: false,
+ showTaskGroupsModifyDeadlineDialog: false,
showTasksDistributeDialog: false,
};
};
@@ -7,29 +10,99 @@ const getDefaultState = () => {
const initialState = getDefaultState();
const getters = {
+ showTaskGroupsAddSolversDialog(state) {
+ return state.showTaskGroupsAddSolversDialog;
+ },
+ showTaskGroupsDeleteDialog(state) {
+ return state.showTaskGroupsDeleteDialog;
+ },
+ showTaskGroupsModifyDeadlineDialog(state) {
+ return state.showTaskGroupsModifyDeadlineDialog;
+ },
showTasksDistributeDialog(state) {
return state.showTasksDistributeDialog;
},
+ taskGroupsByCid(state, getters, rootState, rootGetters) {
+ return (cid) => {
+ return rootGetters['courseware-task-groups/all'].filter(
+ (taskGroup) => taskGroup.relationships.course.data.id === cid
+ );
+ };
+ },
+ tasksByCid(state, getters, rootState, rootGetters) {
+ return (cid) => {
+ const taskGroupIds = getters.taskGroupsByCid(cid).map(({ id }) => id);
+
+ return rootGetters['courseware-tasks/all'].filter((task) =>
+ taskGroupIds.includes(task.relationships['task-group'].data.id)
+ );
+ };
+ },
};
export const state = { ...initialState };
export const actions = {
// setters
+ setShowTaskGroupsAddSolversDialog({ commit }, context) {
+ commit('setShowTaskGroupsAddSolversDialog', context);
+ },
+ setShowTaskGroupsDeleteDialog({ commit }, context) {
+ commit('setShowTaskGroupsDeleteDialog', context);
+ },
+ setShowTaskGroupsModifyDeadlineDialog({ commit }, context) {
+ commit('setShowTaskGroupsModifyDeadlineDialog', context);
+ },
setShowTasksDistributeDialog({ commit }, context) {
commit('setShowTasksDistributeDialog', context);
},
// other actions
+ loadTasksOfCourse({ dispatch }, { cid }) {
+ const options = {
+ 'filter[cid]': cid,
+ include: 'solver, structural-element, task-feedback, task-group, task-group.lecturer',
+ };
+ return dispatch('courseware-tasks/loadAll', { options }, { root: true });
+ },
+
+ loadTaskGroup({ dispatch }, { id }) {
+ const options = {
+ include: 'lecturer',
+ };
+ return dispatch('courseware-task-groups/loadById', { id, options }, { root: true });
+ },
+
+ modifyDeadlineOfTaskGroup({ dispatch }, { taskGroup, endDate }) {
+ taskGroup.attributes['end-date'] = endDate.toISOString();
+
+ return dispatch('courseware-task-groups/update', taskGroup, { root: true });
+ },
+
+ addSolversToTaskGroup({ dispatch, rootGetters }, { taskGroup, solvers }) {
+ return rootGetters.httpClient.post(`courseware-task-groups/${+taskGroup.id}/relationships/solvers`, {
+ data: solvers,
+ });
+ },
};
export const mutations = {
- setShowTasksDistributeDialog(state, data){
+ setShowTaskGroupsAddSolversDialog(state, data) {
+ state.showTaskGroupsAddSolversDialog = data;
+ },
+ setShowTasksDistributeDialog(state, data) {
state.showTasksDistributeDialog = data;
},
+ setShowTaskGroupsDeleteDialog(state, data) {
+ state.showTaskGroupsDeleteDialog = data;
+ },
+ setShowTaskGroupsModifyDeadlineDialog(state, data) {
+ state.showTaskGroupsModifyDeadlineDialog = data;
+ },
};
export default {
+ namespaced: true,
state,
actions,
mutations,
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index 8b2f8da..056802f 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -575,7 +575,7 @@ export const actions = {
element.attributes.commentable = true;
const updatedElement = await dispatch('setStructuralElementComments', { element: element });
-
+
return updatedElement;
},
@@ -584,7 +584,7 @@ export const actions = {
element.attributes.commentable = false;
const updatedElement = await dispatch('setStructuralElementComments', { element: element });
-
+
return updatedElement;
},
@@ -678,7 +678,7 @@ export const actions = {
block.attributes.commentable = true;
const updatedBlock = await dispatch('setBlockComments', { block: block });
-
+
return updatedBlock;
},
@@ -687,7 +687,7 @@ export const actions = {
block.attributes.commentable = false;
const updatedBlock = await dispatch('setBlockComments', { block: block });
-
+
return updatedBlock;
},
diff --git a/tsconfig.json b/tsconfig.json
index 55b45dc..2ada63c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,11 @@
{
"compilerOptions": {
- "target": "es2015",
+ "allowJs": true,
+ "module": "es2020",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
"strict": true,
- "module": "es2015",
- "moduleResolution": "node"
+ "target": "es2020"
},
"include": ["resources/**/*.ts", "resources/**/*.vue"],
"exclude": ["node_modules"]
diff --git a/webpack.common.js b/webpack.common.js
index 3dd376d..8edc9c6 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -172,6 +172,7 @@ module.exports = {
'jquery-ui/widgets/resizable': 'jquery-ui/ui/widgets/resizable',
'@': path.resolve(__dirname, 'resources')
},
+ extensions: ['.ts', '.vue', '.js'],
fallback: {
'stream': require.resolve("stream-browserify"),
'buffer': require.resolve("buffer/")
diff --git a/webpack.dev.js b/webpack.dev.js
index c0cee71..92fb8a8 100644
--- a/webpack.dev.js
+++ b/webpack.dev.js
@@ -1,3 +1,4 @@
+const webpack = require('webpack');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const WebpackNotifierPlugin = require('webpack-notifier');
@@ -10,8 +11,13 @@ const statusesPaths = {
module.exports = merge(common, {
mode: 'development',
- devtool: 'eval',
+ devtool: 'eval-cheap-module-source-map',
plugins: [
+ new webpack.WatchIgnorePlugin({
+ paths:[
+ /\.d\.[cm]ts$/
+ ]
+ }),
new WebpackNotifierPlugin({
appID: 'Stud.IP Webpack',
title: function (params) {