From 332f5ef297815502adbc79df6aa4962688a9aebc Mon Sep 17 00:00:00 2001
From: Viktoria Wiebe <vwiebe@uni-osnabrueck.de>
Date: Tue, 1 Oct 2024 14:07:24 +0000
Subject: [PATCH] Tic 2807 - add toggling task visibility functionality to
 student task dashboard

Closes #2807

Merge request studip/studip!3096
---
 ...6.0.19_tic_2807_add_cw_task_visibility.php | 26 +++++++
 .../JsonApi/Routes/Courseware/Authority.php   |  2 +-
 .../JsonApi/Routes/Courseware/TasksUpdate.php |  4 ++
 .../JsonApi/Schemas/Courseware/Task.php       |  1 +
 lib/models/Courseware/StructuralElement.php   | 18 +++++
 lib/models/Courseware/Task.php                | 22 ++++++
 .../tasks/CoursewareDashboardTasks.vue        | 67 ++++++++++++++++++-
 7 files changed, 138 insertions(+), 2 deletions(-)
 create mode 100644 db/migrations/6.0.19_tic_2807_add_cw_task_visibility.php

diff --git a/db/migrations/6.0.19_tic_2807_add_cw_task_visibility.php b/db/migrations/6.0.19_tic_2807_add_cw_task_visibility.php
new file mode 100644
index 00000000000..4e06c33ebcb
--- /dev/null
+++ b/db/migrations/6.0.19_tic_2807_add_cw_task_visibility.php
@@ -0,0 +1,26 @@
+<?php
+
+
+class Tic2807AddCWTaskVisibility extends Migration
+{
+    public function description()
+    {
+        return 'Adds a visibility column to courseware tasks.';
+    }
+
+    protected function up()
+    {
+        DBManager::get()->exec(
+            "ALTER TABLE `cw_tasks`
+            ADD COLUMN `visible` TINYINT(1) UNSIGNED NOT NULL DEFAULT 0 AFTER `renewal_date`"
+        );
+    }
+
+    protected function down()
+    {
+        DBManager::get()->exec(
+            "ALTER TABLE `cw_tasks`
+            DROP COLUMN `visible`"
+        );
+    }
+}
diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php
index 2acf83e3a6b..7ed609fc65d 100644
--- a/lib/classes/JsonApi/Routes/Courseware/Authority.php
+++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php
@@ -324,7 +324,7 @@ class Authority
 
     public static function canShowTask(User $user, Task $resource): bool
     {
-        return self::canUpdateTask($user, $resource);
+        return self::canUpdateTask($user, $resource) || $resource->visible;
     }
 
     public static function canIndexTasks(User $user): bool
diff --git a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
index 33b51ad1ae8..dd9f648f295 100644
--- a/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
+++ b/lib/classes/JsonApi/Routes/Courseware/TasksUpdate.php
@@ -81,6 +81,10 @@ class TasksUpdate extends JsonApiController
             $resource->requestRenewal();
         }
 
+        if (self::arrayHas($json, 'data.attributes.visible')) {
+            $resource->setVisibility(self::arrayGet($json, 'data.attributes.visible'));
+        }
+
         return $resource;
     }
 
diff --git a/lib/classes/JsonApi/Schemas/Courseware/Task.php b/lib/classes/JsonApi/Schemas/Courseware/Task.php
index 81c7a0d18a5..c612333886c 100644
--- a/lib/classes/JsonApi/Schemas/Courseware/Task.php
+++ b/lib/classes/JsonApi/Schemas/Courseware/Task.php
@@ -36,6 +36,7 @@ class Task extends SchemaProvider
             'submitted' => (bool) $resource['submitted'],
             'renewal' => empty($resource['renewal']) ? null : (string) $resource['renewal'],
             'renewal-date' => date('c', $resource['renewal_date']),
+            'visible' => (bool) $resource['visible'],
             'mkdate' => date('c', $resource['mkdate']),
             'chdate' => date('c', $resource['chdate']),
         ];
diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php
index e72cc4f34ef..8e06a396af9 100644
--- a/lib/models/Courseware/StructuralElement.php
+++ b/lib/models/Courseware/StructuralElement.php
@@ -391,6 +391,19 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject, \Feedbac
                         return true;
                     }
 
+                    if ($task->isSubmitted()) {
+                        if ($task->visible) {
+                            return true;
+                        }
+                        $solvers = $task->getTaskGroup()->getSolvers();
+                        foreach ($solvers as $solver) {
+                            if ($solver->id === $user->id) {
+                                return true;
+                            }
+                        }
+                        return false;
+                    }
+
                     return $task->userIsASolver($user);
                 }
 
@@ -1240,4 +1253,9 @@ SQL;
             [$this->id, self::class]
         );
     }
+
+    public function isTaskVisible(): bool
+    {
+        return $this->payload['task-visibility'];
+    }
 }
diff --git a/lib/models/Courseware/Task.php b/lib/models/Courseware/Task.php
index d409676ca3c..2a1a28d2b00 100644
--- a/lib/models/Courseware/Task.php
+++ b/lib/models/Courseware/Task.php
@@ -21,6 +21,7 @@ use User;
  * @property int $submitted database column
  * @property string|null $renewal database column
  * @property int $renewal_date database column
+ * @property int $visible database column
  * @property int|null $feedback_id database column
  * @property int $mkdate database column
  * @property int $chdate database column
@@ -90,6 +91,11 @@ class Task extends \SimpleORMap
         parent::configure($config);
     }
 
+    public function getTaskGroup(): TaskGroup
+    {
+        return $this->task_group;
+    }
+
     /**
      * Returns the structural element of this task.
      * This structural element and all its children are part of the task.
@@ -130,6 +136,16 @@ class Task extends \SimpleORMap
                 if ($this->solver_id === $user->id) {
                     return true;
                 }
+
+                if ($this->visible) {
+                    $solvers = $this->getTaskGroup()->getSolvers();
+                    foreach ($solvers as $solver) {
+                        if ($solver->id === $user->id) {
+                            return true;
+                        }
+                    }
+                }
+
                 break;
 
             case 'group':
@@ -235,6 +251,12 @@ class Task extends \SimpleORMap
         $this->store();
     }
 
+    public function setVisibility(bool $visibility): void
+    {
+        $this->visible = (int) $visibility;
+        $this->store();
+    }
+
     private function getStructuralElementProgress(StructuralElement $structural_element): float
     {
         $containers = Container::findBySQL('structural_element_id = ?', [intval($structural_element->id)]);
diff --git a/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
index 35384fce41f..cb3577ad563 100644
--- a/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareDashboardTasks.vue
@@ -2,7 +2,15 @@
     <div class="cw-dashboard-tasks-wrapper">
         <table v-if="tasks.length > 0" class="default">
             <colgroup>
-                <col />
+                <col style="width: 5%" />
+                <col style="width: 20%" />
+                <col style="width: 10%" />
+                <col style="width: 10%" />
+                <col style="width: 5%" />
+                <col style="width: 15%" />
+                <col style="width: 15%" />
+                <col style="width: 15%" />
+                <col style="width: 5%" />
             </colgroup>
             <thead>
                 <tr>
@@ -12,6 +20,7 @@
                     <th>{{ $gettext('Abgabefrist') }}</th>
                     <th>{{ $gettext('Abgabe') }}</th>
                     <th class="responsive-hidden">{{ $gettext('Verlängerungsanfrage') }}</th>
+                    <th class="responsive-hidden">{{ $gettext('Für Teilnehmende freigeben') }}</th>
                     <th class="responsive-hidden">{{ $gettext('Anmerkung') }}</th>
                     <th class="actions">{{ $gettext('Aktionen') }}</th>
                 </tr>
@@ -54,6 +63,23 @@
                             {{ $gettext('verlängert bis') }}: {{ getReadableDate(task.attributes['renewal-date']) }}
                         </span>
                     </td>
+                    <td class="responsive-hidden">
+                        <span v-if="task.attributes.submitted">
+                            <button
+                                class="button"
+                                v-if="!task.attributes.visible"
+                                @click="toggleVisibilityOn(task)"
+                            >
+                                {{ $gettext('Freigeben') }}
+                            </button>
+                            <button
+                                class="button"
+                                v-if="task.attributes.visible"
+                                @click="toggleVisibilityOff(task)">
+                                {{ $gettext('Freigabe widerrufen') }}
+                            </button>
+                        </span>
+                    </td>
                     <td class="responsive-hidden">
                         <studip-icon
                             v-if="feedback"
@@ -166,6 +192,13 @@ export default {
                 return result;
             });
         },
+        taskVisibilities() {
+            let visibilities = [];
+            for (const task of this.tasks) {
+                visibilities[`${task.task.id}`] = task.element.attributes.payload['task-visibility'];
+            }
+            return visibilities;
+        }
     },
     methods: {
         ...mapActions({
@@ -175,6 +208,7 @@ export default {
             companionSuccess: 'companionSuccess',
             companionError: 'companionError',
             createCoursewareUnit: 'courseware-units/create',
+            loadStructuralElement: 'courseware-structural-elements/loadById'
         }),
         getTaskMenuItems(task, status, element) {
             let menuItems = [];
@@ -277,6 +311,37 @@ export default {
             this.showFeedbackDialog = true;
             this.currentTaskFeedback = feedback.attributes.content;
         },
+        toggleVisibilityOn(task) {
+            let attributes = task.attributes;
+            attributes['visible'] = true;
+            this.toggleVisibility(task, attributes);
+        },
+        toggleVisibilityOff(task) {
+            let attributes = task.attributes;
+            attributes['visible'] = false;
+            this.toggleVisibility(task, attributes);
+        },
+        async toggleVisibility(task, attributes) {
+            await this.updateTask({
+                attributes: attributes,
+                taskId: task.id,
+            });
+
+            const taskGroup = this.getTaskGroupById({ id: task.relationships['task-group'].data.id });
+            const taskTitle = taskGroup.attributes.title;
+
+            if (attributes.visible) {
+                this.companionSuccess({
+                    info: this.$gettextInterpolate(this.$gettext('"%{ title }" wurde freigegeben.'),
+                    { title: taskTitle }),
+                });
+            } else {
+                this.companionSuccess({
+                    info: this.$gettextInterpolate(this.$gettext('Die Freigabe für %{ "title }" wurde zurückgenommen.'),
+                    { title: taskTitle }),
+                });
+            }
+        }
     },
 };
 </script>
-- 
GitLab