From 330ddc50a3fcbbad28d34fa94ae5f06207b02494 Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Tue, 15 Nov 2022 13:42:41 +0000
Subject: [PATCH] allow profile categories to be translated, fixes #1260

Closes #1260

Merge request studip/studip!769
---
 app/controllers/settings/categories.php | 33 +++++----
 app/views/settings/categories.php       | 92 ++++++++++++-------------
 lib/models/Kategorie.class.php          | 43 ++++++++----
 lib/models/User.class.php               | 10 ++-
 4 files changed, 100 insertions(+), 78 deletions(-)

diff --git a/app/controllers/settings/categories.php b/app/controllers/settings/categories.php
index f62366498d2..6e8bbab8fc6 100644
--- a/app/controllers/settings/categories.php
+++ b/app/controllers/settings/categories.php
@@ -135,23 +135,26 @@ class Settings_CategoriesController extends Settings_SettingsController
      */
     public function store_action()
     {
-        $request = Request::getInstance();
         $changed = false;
 
-        $categories = $request['categories'];
-        foreach ($categories as $id => $data) {
-            if (empty($data['name'])) {
-                PageLayout::postError(_('Kategorien ohne Namen können nicht gespeichert werden!'));
-                continue;
-            }
-            $category = Kategorie::find($id);
-            $category->name    = $data['name'];
-            $category->content = Studip\Markup::purifyHtml($data['content']);
-            if ($category->isDirty() && $category->store()) {
-                $changed = true;
-                Visibility::renamePrivacySetting('kat_' . $category->id, $category->name);
-            }
-        }
+        Kategorie::findEachMany(
+            function (Kategorie $category)  use (&$changed) {
+                $category->name    = Request::i18n("category-name-{$category->id}");
+                $category->content = Request::i18n(
+                    "category-content-{$category->id}",
+                    null,
+                    function ($input) {
+                        return Studip\Markup::purifyHtml($input);
+                    }
+                );
+
+                if ($category->store()) {
+                    $changed = true;
+                    Visibility::renamePrivacySetting("kat_{$category->id}", $category->name->original());
+                }
+            },
+            Request::optionArray('ids')
+        );
 
         if ($changed) {
             PageLayout::postSuccess(_('Kategorien geändert!'));
diff --git a/app/views/settings/categories.php b/app/views/settings/categories.php
index 663b54f6147..12e9df58970 100644
--- a/app/views/settings/categories.php
+++ b/app/views/settings/categories.php
@@ -8,61 +8,57 @@
     <input type="hidden" name="studip_ticket" value="<?= get_ticket() ?>">
 
     <? foreach ($categories as $index => $category): ?>
+        <input type="hidden" name="ids[]" value="<?= htmlReady($category->id) ?>">
+
         <fieldset>
-            <legend><?= htmlReady($category->name) ?></legend>
+            <legend style="display: flex; flex-wrap: nowrap; justify-content: space-between">
+                <span><?= htmlReady($category->name) ?></span>
+                <span>
+                <? if ($index > 0): ?>
+                    <a href="<?= $controller->url_for('settings/categories/swap', $category->id, $last->id) ?>">
+                        <?= Icon::create('arr_2up', 'sort')->asImg(['class' => 'text-top', 'title' =>_('Kategorie nach oben verschieben')]) ?>
+                    </a>
+                <? else: ?>
+                    <?= Icon::create('arr_2up', 'inactive')->asImg(['class' => 'text-top']) ?>
+                <? endif; ?>
 
-            <table style="width: 100%">
-                <colgroup>
-                    <col>
-                    <col width="100px">
-                </colgroup>
-                <tbody>
-                    <tr>
-                        <td>
-                            <div>
-                                (<?= $visibilities[$category->id] ?>)
-                            </div>
+                <? if ($index < $count - 1): ?>
+                    <a href="<?= $controller->url_for('settings/categories/swap', $category->id, $categories[$index + 1]->id) ?>">
+                                <?= Icon::create('arr_2down', 'sort')->asImg(['class' => 'text-top', 'title' =>_('Kategorie nach unten verschieben')]) ?>
+                            </a>
+                <? else: ?>
+                    <?= Icon::create('arr_2down', 'inactive')->asImg(['class' => 'text-top']) ?>
+                <? endif; ?>
 
-                            <label>
-                                <?= _('Name') ?>
-                                <input required type="text" name="categories[<?= $category->id ?>][name]" id="name<?= $index ?>"
-                                       aria-label="<?= _('Name der Kategorie') ?>" style="width: 100%"
-                                       value="<?= htmlReady($category->name) ?>">
-                            </label>
+                    <a href="<?= $controller->url_for('settings/categories/delete', $category->id) ?>">
+                        <?= Icon::create('trash')->asImg(['class' => 'text-top', 'title' => _('Kategorie löschen')]) ?>
+                    </a>
+                </span>
+            </legend>
 
-                            <label>
-                                <?= _('Inhalt') ?>
+            <p>
+                (<?= $visibilities[$category->id] ?>)
+            </p>
 
-                                <textarea id="content<?= $index ?>" name="categories[<?= $category->id ?>][content]"
-                                          class="resizable add_toolbar wysiwyg size-l" style="width: 100%; height: 200px;"
-                                          aria-label="<?= _('Inhalt der Kategorie:') ?>"
-                                ><?= wysiwygReady($category->content) ?></textarea>
-                            </label>
-                        </td>
-                        <td style="vertical-align: top">
-                            <? if ($index > 0): ?>
-                                <a href="<?= $controller->url_for('settings/categories/swap', $category->id, $last->id) ?>">
-                                    <?= Icon::create('arr_2up', 'sort')->asImg(['class' => 'text-top', 'title' =>_('Kategorie nach oben verschieben')]) ?>
-                                </a>
-                            <? else: ?>
-                                <?= Icon::create('arr_2up', 'inactive')->asImg(['class' => 'text-top']) ?>
-                            <? endif; ?>
+            <label>
+                <?= _('Name') ?>
+                <?= I18N::input("category-name-{$category->id}", $category->name, [
+                    'aria-label' => _('Name der Kategorie'),
+                    'class'      => 'size-l',
+                    'id'         => "name{$index}",
+                    'required'   => '',
+                ]) ?>
+            </label>
 
-                            <? if ($index < $count - 1): ?>
-                                <a href="<?= $controller->url_for('settings/categories/swap', $category->id, $categories[$index + 1]->id) ?>">
-                                    <?= Icon::create('arr_2down', 'sort')->asImg(['class' => 'text-top', 'title' =>_('Kategorie nach unten verschieben')]) ?>
-                                </a>
-                            <? else: ?>
-                                <?= Icon::create('arr_2down', 'inactive')->asImg(['class' => 'text-top']) ?>
-                            <? endif; ?>
+            <label>
+                <?= _('Inhalt') ?>
 
-                            <a href="<?= $controller->url_for('settings/categories/delete', $category->id) ?>">
-                                <?= Icon::create('trash')->asImg(['class' => 'text-top', 'title' => _('Kategorie löschen')]) ?>
-                            </a>
-                        </td>
-                    </tr>
-                </tbody>
-            </table>
+                <?= I18n::textarea("category-content-{$category->id}", $category->content, [
+                    'aria-label' => _('Inhalt der Kategorie:'),
+                    'class'      => 'resizable add_toolbar wysiwyg size-l',
+                    'id'         => "content{$index}",
+                ]) ?>
+            </label>
         </fieldset>
     <? $last = $category;
        endforeach; ?>
diff --git a/lib/models/Kategorie.class.php b/lib/models/Kategorie.class.php
index 890009ce73f..13c9c3a464f 100644
--- a/lib/models/Kategorie.class.php
+++ b/lib/models/Kategorie.class.php
@@ -1,5 +1,5 @@
-<?
-/*
+<?php
+/**
  * Kategorie model
  *
  * This program is free software; you can redistribute it and/or
@@ -12,14 +12,16 @@
  * @category    Stud.IP
  * @since       2.4
  *
- * @property string kategorie_id database column
- * @property string id alias column for kategorie_id
- * @property string range_id database column
- * @property string name database column
- * @property string content database column
- * @property string mkdate database column
- * @property string chdate database column
- * @property string priority database column
+ * @property string $kategorie_id database column
+ * @property string $id alias column for kategorie_id
+ * @property string $range_id database column
+ * @property string|I18NString $name database column
+ * @property string|I18NString $content database column
+ * @property int $mkdate database column
+ * @property int $chdate database column
+ * @property int $priority database column
+ *
+ * @property User $user
  */
 
 class Kategorie extends SimpleORMap
@@ -32,6 +34,19 @@ class Kategorie extends SimpleORMap
     protected static function configure($config = [])
     {
         $config['db_table'] = 'kategorien';
+
+        $config['belongs_to'] = [
+            'user' => [
+                'class_name'  => User::class,
+                'foreign_key' => 'range_id',
+            ],
+        ];
+
+        $config['i18n_fields'] = [
+            'name' => true,
+            'content' => true,
+        ];
+
         parent::configure($config);
     }
 
@@ -39,9 +54,9 @@ class Kategorie extends SimpleORMap
      * Finds all categories of a specific user
      *
      * @param string $user_id Id of the user
-     * @return array of category objects
+     * @return Kategorie[] of category objects
      */
-    public static function findByUserId($user_id)
+    public static function findByUserId(string $user_id): array
     {
         return self::findByRange_id($user_id, 'ORDER BY priority');
     }
@@ -50,9 +65,9 @@ class Kategorie extends SimpleORMap
      * Increases all category priorities of a user
      *
      * @param string $user_id Id of the user
-     * @return number of changed records
+     * @return bool indicating if anything has changed
      */
-    public static function increasePrioritiesByUserId($user_id)
+    public static function increasePrioritiesByUserId(string $user_id): bool
     {
         $query = "UPDATE kategorien SET priority = priority + 1 WHERE range_id = ?";
         $statement = DBManager::get()->prepare($query);
diff --git a/lib/models/User.class.php b/lib/models/User.class.php
index 30c38ad19dd..3d6a7e0a904 100644
--- a/lib/models/User.class.php
+++ b/lib/models/User.class.php
@@ -67,6 +67,7 @@
  * @property SimpleORMapCollection contacts has_many Contact
  * @property UserInfo   info   has_one UserInfo
  * @property UserOnline online has_one UserOnline
+ * @property Kategorie[]|SimpleORMapCollection $profile_categories has_many Kategorie
  */
 class User extends AuthUserMd5 implements Range, PrivacyObject
 {
@@ -158,11 +159,18 @@ class User extends AuthUserMd5 implements Range, PrivacyObject
             'class_name' => ConsultationBooking::class,
             'on_delete'  => 'delete',
         ];
+        $config['has_many']['profile_categories'] = [
+            'class_name'        => Kategorie::class,
+            'assoc_foreign_key' => 'range_id',
+            'on_delete'         => 'delete',
+        ];
+
 
         $config['has_many']['mvv_assignments'] = [
             'class_name'        => MvvContact::class,
             'assoc_foreign_key' => 'contact_id',
             'on_delete'         => 'delete',
+            'order_by'          => 'ORDER BY priority',
         ];
 
         $config['has_and_belongs_to_many']['domains'] = [
@@ -874,7 +882,7 @@ class User extends AuthUserMd5 implements Range, PrivacyObject
             }
         }
 
-        foreach (Kategorie::findByUserId($this->id) as $category) {
+        foreach ($this->profile_categories as $category) {
             $homepage_elements['kat_' . $category->id] = [
                 'name'       => $category->name,
                 'visibility' => $homepage_visibility['kat_' . $category->id] ?: get_default_homepage_visibility($this->id),
-- 
GitLab