Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alexander.vorwerk/studip
  • hochschule-wismar/stud-ip
  • tleilax/studip
  • marcus/studip
  • manschwa/studip
  • eberhardt/studip
  • uol/studip
  • pluta/studip
  • thienel/extern-uni-b
  • studip/studip
  • strohm/studip
  • uni-osnabrueck/studip
  • FloB/studip
  • universit-t-rostock/studip
  • Robinyyy/studip
  • jakob.diel/studip
  • HyperSpeeed/studip
  • ann/studip
  • nod3zer0/stud-ip-siple-saml-php-plugin
19 results
Show changes
Commits on Source (179)
Showing
with 2288 additions and 478 deletions
*~
.env
.webpack.*
composer
node_modules
......@@ -40,6 +42,9 @@ tests/_helpers/TestGuy.php
tests/_helpers/WebGuy.php
tests/_helpers/_generated
tests/_output/
tests/e2e/.auth
tests/e2e/test-results/*
playwright-report/*
.idea
/config/oauth2/*.key
......
image: studip/studip:tests-php7.2
variables:
FF_NETWORK_PER_BUILD: 1
GIT_DEPTH: 1
MYSQL_RANDOM_ROOT_PASSWORD: "true"
MYSQL_DATABASE: studip_db
......@@ -16,18 +17,20 @@ variables:
# Use faster docker driver
DOCKER_DRIVER: overlay2
# Images
NODE_IMAGE: node:14-slim
NODE_IMAGE: node:16-slim
# Directories
CACHE_DIR: .caches
REPORT_DIR: .reports
# Set npm cache directory
npm_config_cache: $CI_PROJECT_DIR/.npm
stages:
- build
- checks
- analyse
- test
- packaging
- release
- build
.scripts:
mkdir-caches: &mkdir-caches
......@@ -47,38 +50,60 @@ stages:
- cli/studip migrate
.caches:
php: &composer-cache
key: "php-$CI_COMMIT_REF_SLUG"
composer: &composer-cache
key:
files:
- composer.lock
paths:
- composer/
- $CACHE_DIR/phplint-cache
- $CACHE_DIR/resultCache.php
- $CACHE_DIR/cache/*
- $CACHE_DIR/resultCaches/*
js: &npm-cache
key: "js-$CI_COMMIT_REF_SLUG"
policy: pull
npm: &npm-cache
key:
files:
- package-lock.json
paths:
- node_modules/
- $CACHE_DIR/eslint-cache
- $CACHE_DIR/stylelint-cache
- .npm
build-composer:
stage: build
needs: []
interruptible: true
variables:
COMPOSER_CACHE: $CACHE_DIR/composer-cache
before_script:
- mkdir -p $COMPOSER_CACHE
script:
- composer install
cache:
- *composer-cache
- key: composer-package-cache
paths:
- $COMPOSER_CACHE
policy: pull-push
lint-php:
stage: checks
needs: []
cache: *composer-cache
needs: [build-composer]
variables:
CACHE_LOCATION: $CACHE_DIR/phplint-cache
PHPLINT_JSON_REPORT: $REPORT_DIR/phplint-report.json
PHPLINT_CODE_QUALITY_REPORT: $REPORT_DIR/phplint-codequality.json
interruptible: true
cache:
- *composer-cache
- key: "$CI_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
paths:
- $CACHE_LOCATION
before_script:
- *mkdir-caches
- *mkdir-reports
- *install-composer
script:
- php -d memory_limit=-1
composer/bin/phplint
- COMPOSER_MEMORY_LIMIT=-1
composer exec phplint
--
--json $PHPLINT_JSON_REPORT
--cache=$CACHE_DIR/phplint-cache
--cache=$CACHE_LOCATION
after_script:
- ./.gitlab/scripts/convert-phplint-report $PHPLINT_JSON_REPORT > $PHPLINT_CODE_QUALITY_REPORT
artifacts:
......@@ -90,10 +115,16 @@ lint-js:
needs: []
image: $NODE_IMAGE
variables:
CACHE_LOCATION: $CACHE_DIR/eslint-cache
ESLINT_CODE_QUALITY_REPORT: $REPORT_DIR/eslint-codequality.json
cache:
- key: "$CI_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
paths:
- $CACHE_LOCATION
interruptible: true
before_script:
- *mkdir-reports
- npm install -g npm@7
- npm install
--no-save --no-audit --no-fund
--loglevel=error
......@@ -101,6 +132,7 @@ lint-js:
script:
- npx eslint
--ext .js,.vue
--cache --cache-location $CACHE_LOCATION
--format gitlab
resources/assets/javascripts resources/vue
artifacts:
......@@ -112,8 +144,13 @@ lint-css:
needs: []
image: $NODE_IMAGE
variables:
CACHE_LOCATION: $CACHE_DIR/stylelint-cache
STYLELINT_CODE_QUALITY_REPORT: $REPORT_DIR/stylelint-codequality.json
interruptible: true
cache:
- key: "$CI_JOB_NAME_SLUG:CI_COMMIT_REF_SLUG"
paths:
- $CACHE_LOCATION
before_script:
- *mkdir-reports
- npm install
......@@ -125,7 +162,9 @@ lint-css:
script:
- npx
stylelint
--cache --cache-location $CACHE_LOCATION
--custom-formatter=node_modules/stylelint-formatter-gitlab
--output-file $STYLELINT_CODE_QUALITY_REPORT
resources/assets/stylesheets
artifacts:
reports:
......@@ -133,16 +172,23 @@ lint-css:
phpstan:
stage: analyse
needs: [lint-php]
needs: [build-composer]
variables:
CACHE_LOCATION: $CACHE_DIR/phpstan
PHPSTAN_CODE_QUALITY_REPORT: $REPORT_DIR/phpstan-codequality.json
allow_failure: true
interruptible: true
when: manual
cache: *composer-cache
cache:
- *composer-cache
- key: "$CO_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
paths:
- $CACHE_LOCATION
before_script:
- *mkdir-caches
- *mkdir-reports
- *install-composer
- 'echo "includes:\n - phpstan.neon.dist\n\nparameters:\n tmpDir: $PHPSTAN_CACHE_PATH" > phpstan.neon'
script:
- php
composer/bin/phpstan analyse
......@@ -150,6 +196,8 @@ phpstan:
--no-progress
--level=$PHPSTAN_LEVEL
--error-format=gitlab > $PHPSTAN_CODE_QUALITY_REPORT
after_script:
- rm phpstan.neon
artifacts:
reports:
codequality: $PHPSTAN_CODE_QUALITY_REPORT
......@@ -178,6 +226,22 @@ test-unit:
reports:
junit: $PHPUNIT_XML_REPORT
test-jest:
stage: test
needs: [lint-js]
variables:
JS_TEST_REPORT: $REPORT_DIR/jest.xml
cache: *npm-cache
interruptible: true
before_script:
- *mkdir-reports
- npm install
script:
- JEST_JUNIT_OUTPUT_FILE="$JS_TEST_REPORT" npx jest tests/jest/ --ci --reporters=default --reporters=jest-junit
artifacts:
reports:
junit: $JS_TEST_REPORT
test-functional:
stage: test
needs: [lint-php]
......@@ -241,6 +305,55 @@ test-assets:
script:
- npm run webpack-dev
test-e2e:
stage: test
# needs: [lint-css, lint-js, lint-php]
image: mcr.microsoft.com/playwright:v1.33.0-jammy
services:
- mariadb
variables:
PHP_WEBSERVER_URL: localhost:65432
E2E_REPORT: $REPORT_DIR/e2e.xml
interruptible: true
when: manual
cache:
- *composer-cache
- *npm-cache
before_script:
- mkdir ./bin
- apt-get update
- apt -y install software-properties-common
- add-apt-repository ppa:ondrej/php
- apt-get update
- DEBIAN_FRONTEND=noninteractive
apt-get -yq install
make zip unzip mariadb-client
php7.4 libapache2-mod-php7.4 php7.4-common php7.4-curl php7.4-mbstring
php7.4-xmlrpc php7.4-mysql php7.4-gd php7.4-xml php7.4-intl php7.4-ldap
php7.4-imagick php7.4-json php7.4-cli
- echo "short_open_tag=On" >> /etc/php/7.4/php.ini
- echo "short_open_tag=On" >> /etc/php/7.4/cli/php.ini
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php --install-dir=./bin --filename=composer
- export PATH="./bin:$PATH"
- php -r "unlink('composer-setup.php');"
- *mkdir-reports
- *initialize-studip-database
- ./cli/studip config:set SHOW_TERMS_ON_FIRST_LOGIN 0
- npm install playwright
- npm ci
- npx playwright install --with-deps
script:
- php -S $PHP_WEBSERVER_URL -t public -q &
- PHP_SERVER_PID=$!
- PLAYWRIGHT_JUNIT_OUTPUT_NAME="$E2E_REPORT"
PLAYWRIGHT_BASE_URL="http://$PHP_WEBSERVER_URL"
npx playwright test --reporter=junit --grep-invert a11y
- kill -3 $PHP_SERVER_PID
artifacts:
reports:
junit: $E2E_REPORT
packaging:
stage: packaging
cache: []
......@@ -263,6 +376,17 @@ packaging:
dotenv: .packaging.env
expire_in: never
build_image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
stage: build
when: manual
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
script:
- /kaniko/executor --context=dir://${CI_PROJECT_DIR} --dockerfile ${CI_PROJECT_DIR}/docker/studip/Dockerfile --destination ${IMAGE_TAG} --cache=true
release:
stage: release
image: studip/release-cli
......
......@@ -6,4 +6,4 @@ Welches Problem ist aufgetreten und wie ist es dazu gekommen? (Hilfreich: vorher
Ist der Fehler reproduzierbar? Falls ja, wie.
/label ~BIEST ~"Version::5.3"
/label ~BIEST ~"Version::5.4"
This diff is collapsed.
......@@ -72,13 +72,27 @@ test-functional: $(CODECEPT)
test-jsonapi: $(CODECEPT)
$(CODECEPT) run jsonapi
test-e2e: npm
npx playwright test
test-unit: $(CODECEPT)
$(CODECEPT) run unit
catalogs: npm $(CATALOGS)
clean-icons:
find public/assets/images/icons -type f -not -path '*blue*' -delete
optimize-icons: npm
find public/assets/images/icons -type f | xargs -P0 npx svgo -q --config=config/svgo.config.js
find public/assets/images/icons/blue -type f | xargs -P0 npx svgo -q --config=config/svgo.config.js
icons: optimize-icons
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#000000/" {} > {}' | sed 's#icons/blue#icons/black#2' | sh
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#00962d/" {} > {}' | sed 's#icons/blue#icons/green#2' | sh
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#6e6e6e/" {} > {}' | sed 's#icons/blue#icons/grey#2' | sh
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#cb1800/" {} > {}' | sed 's#icons/blue#icons/red#2' | sh
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#ffffff/" {} > {}' | sed 's#icons/blue#icons/white#2' | sh
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#ffad00/" {} > {}' | sed 's#icons/blue#icons/yellow#2' | sh
# default rules for gettext handling
js-%.pot: $(VUE_SOURCES)
......
# Stud.IP v5.4
**<Datum>**
**23.05.23**
## Neue Features
-
- Neben dem Vollbild-Modus (eingeführt in der Version 5.0), der nur in bestimmten Kontexten gezeigt wird, gibt es nun einen Modus "kompakte Navigation". Der neue Modus wird über das bisherige Icon für den Vollbildmodus aktiviert. Bitte passen Sie ihre Dokumentationen an.
## Breaking changes
......@@ -16,4 +16,8 @@
## Deprecated Features
-
- Das Verwenden von LESS-Stylesheets in Plugins wurde deprecated und wird zu Stud.IP 6.0 entfernt werden. Die betroffenen Plugins müssen angepasst und auf SCSS umgestellt werden.
## Known Issues
- Der Vollbildmodus funktioniert nicht auf Apple iPads. Der Modus kann zwar initiiert werden, beendet sich aber selbsständig, wenn nach oben gescrollt wird. Dieses Verhalten ist en Fehler innerhalb von iOS/iPadOS und kann seitens Stud.IP nicht umgangen werden. Der Fehler ist bei Apple gemeldet.
......@@ -126,7 +126,7 @@ class Accessibility_FormsController extends StudipController
//Get the sender and their language:
$sender = User::findCurrent();
//Default to the system default language:
$lang = explode('_', $GLOBALS['DEFAULT_LANGUAGE'])[0];
$lang = explode('_', Config::get()->DEFAULT_LANGUAGE ?? 'de_DE')[0];
if ($sender) {
//Use the senders language since the choices in the form
//are in their language as well.
......
This diff is collapsed.
......@@ -61,24 +61,14 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
/**
* Displays all available schedules according to the set filters.
*
* @param int $page Which page to display
*/
public function index_action($page = 0)
public function index_action()
{
$filter = $_SESSION['cronjob-filter'];
$this->total = CronjobSchedule::countBySql('1');
$this->pagination = Pagination::create(
CronjobSchedule::countBySql($filter['where']),
$page
);
$this->schedules = $this->pagination->loadSORMCollection(
CronjobSchedule::class,
$filter['where']
);
$this->schedules = CronjobSchedule::findBySQL($filter['where']);
// Filters
$this->tasks = CronjobTask::findBySql('1');
......@@ -134,9 +124,8 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
* Edits a schedule.
*
* @param String $id Id of the schedule in question (null to create)
* @param int $page Return to this page after editing (optional)
*/
public function edit_action(CronjobSchedule $schedule = null, $page = 0)
public function edit_action(CronjobSchedule $schedule = null)
{
if (Request::submitted('store')) {
$parameters = Request::getArray('parameters');
......@@ -173,7 +162,7 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
$schedule->store();
PageLayout::postSuccess(_('Die Änderungen wurden gespeichert.'));
$this->redirect('admin/cronjobs/schedules/index/' . $page);
$this->redirect('admin/cronjobs/schedules/index');
return;
}
......@@ -182,11 +171,10 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
$actions = Sidebar::get()->addWidget(new ActionsWidget());
$actions->addLink(
_('Zurück zur Übersicht'),
$this->indexURL($page),
$this->indexURL(),
Icon::create('link-intern')
);
$this->page = $page;
$this->tasks = CronjobTask::findBySql('1');
}
......@@ -214,56 +202,51 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
* Activates a schedule.
*
* @param CronjobSchedule $schedule Schedule to activate
* @param int $page Return to this page after activating (optional)
*/
public function activate_action(CronjobSchedule $schedule, $page = 0)
public function activate_action(CronjobSchedule $schedule)
{
$schedule->activate();
if (!Request::isXhr()) {
PageLayout::postSuccess(_('Der Cronjob wurde aktiviert.'));
}
$this->redirect("admin/cronjobs/schedules/index/{$page}#job-{$schedule->id}");
$this->redirect("admin/cronjobs/schedules/index#job-{$schedule->id}");
}
/**
* Deactivates a schedule.
*
* @param CronjobSchedule $schedule Schedule to deactivate
* @param int $page Return to this page after deactivating (optional)
*/
public function deactivate_action(CronjobSchedule $schedule, $page = 0)
public function deactivate_action(CronjobSchedule $schedule)
{
$schedule->deactivate();
if (!Request::isXhr()) {
PageLayout::postSuccess(_('Der Cronjob wurde deaktiviert.'));
}
$this->redirect("admin/cronjobs/schedules/index/{$page}#job-{$schedule->id}");
$this->redirect("admin/cronjobs/schedules/index#job-{$schedule->id}");
}
/**
* Cancels/deletes a schedule.
*
* @param CronjobSchedule $schedule Schedule to cancel
* @param int $page Return to this page after canceling (optional)
*/
public function cancel_action(CronjobSchedule $schedule, $page = 0)
public function cancel_action(CronjobSchedule $schedule)
{
CSRFProtection::verifyUnsafeRequest();
$schedule->delete();
PageLayout::postSuccess(_('Der Cronjob wurde gelöscht.'));
$this->redirect("admin/cronjobs/schedules/index/{$page}");
$this->redirect('admin/cronjobs/schedules/index');
}
/**
* Performs a bulk operation on a set of schedules. Operation can be
* either activating, deactivating or canceling/deleting.
*
* @param int $page Return to this page afterwarsd (optional)
*/
public function bulk_action($page = 0)
public function bulk_action()
{
$action = Request::option('action');
$ids = Request::optionArray('ids');
......@@ -310,6 +293,6 @@ class Admin_Cronjobs_SchedulesController extends AuthenticatedController
PageLayout::postSuccess($message);
}
$this->redirect("admin/cronjobs/schedules/index/{$page}");
$this->redirect('admin/cronjobs/schedules/index');
}
}
......@@ -47,26 +47,18 @@ class Admin_Cronjobs_TasksController extends AuthenticatedController
/**
* Displays all available tasks.
*
* @param int $page Which page to display
*/
public function index_action($page = 0)
public function index_action()
{
$this->pagination = Pagination::create(
CronjobTask::countBySql('1'),
$page
);
$this->tasks = $this->pagination->loadSORMCollection(CronjobTask::class);
$this->tasks = CronjobTask::findBySQL('1');
}
/**
* Activates a tasks.
*
* @param CronjobTask $task Task to activate
* @param int $page Return to this page after activating (optional)
*/
public function activate_action(CronjobTask $task, $page = 0)
public function activate_action(CronjobTask $task)
{
$task->active = true;
$task->store();
......@@ -80,16 +72,15 @@ class Admin_Cronjobs_TasksController extends AuthenticatedController
$message = sprintf(_('Die Aufgabe und %u Cronjob(s) wurden aktiviert.'), $activated);
PageLayout::postSuccess($message);
}
$this->redirect("admin/cronjobs/tasks/index/{$page}#task-{$task->id}");
$this->redirect("admin/cronjobs/tasks/index#task-{$task->id}");
}
/**
* Deactivates a tasks.
*
* @param CronjobTask $task Task to deactivate
* @param int $page Return to this page after deactivating (optional)
*/
public function deactivate_action(CronjobTask $task, $page = 0)
public function deactivate_action(CronjobTask $task)
{
$task->active = false;
$task->store();
......@@ -103,16 +94,15 @@ class Admin_Cronjobs_TasksController extends AuthenticatedController
$message = sprintf(_('Die Aufgabe und %u Cronjob(s) wurden deaktiviert.'), $deactivated);
PageLayout::postSuccess($message);
}
$this->redirect("admin/cronjobs/tasks/index/{$page}#task-{$task->id}");
$this->redirect("admin/cronjobs/tasks/index#task-{$task->id}");
}
/**
* Deletes a tasks.
*
* @param CronjobTask $task Task to delete
* @param int $page Return to this page after deleting (optional)
*/
public function delete_action(CronjobTask $task, $page = 0)
public function delete_action(CronjobTask $task)
{
CSRFProtection::verifyUnsafeRequest();
$deleted = $task->schedules->count();
......@@ -121,16 +111,14 @@ class Admin_Cronjobs_TasksController extends AuthenticatedController
$message = sprintf(_('Die Aufgabe und %u Cronjob(s) wurden gelöscht.'), $deleted);
PageLayout::postSuccess($message);
$this->redirect("admin/cronjobs/tasks/index/{$page}");
$this->redirect('admin/cronjobs/tasks/index');
}
/**
* Performs a bulk operation on a set of tasks. Operation can be either
* activating, deactivating or deleting.
*
* @param int $page Return to this page afterwarsd (optional)
*/
public function bulk_action($page = 0)
public function bulk_action()
{
$action = Request::option('action');
$ids = Request::optionArray('ids');
......@@ -170,7 +158,7 @@ class Admin_Cronjobs_TasksController extends AuthenticatedController
PageLayout::postSuccess($message);
}
$this->redirect('admin/cronjobs/tasks/index/' . $page);
$this->redirect('admin/cronjobs/tasks/index');
}
/**
......
......@@ -571,4 +571,49 @@ class Admin_PluginController extends AuthenticatedController
}
}
public function edit_description_action(Plugin $plugin)
{
$this->plugin = PluginManager::getInstance()->getPluginById($plugin->getId());
$this->metadata = $this->plugin->getMetadata();
$this->form = \Studip\Forms\Form::fromSORM($plugin, [
'legend' => _('Pluginbeschreibung'),
'fields' => [
'description' => [
'label' => _('Beschreibung'),
'type' => 'i18n_formatted'
],
'manifest_info_de' => [
'label' => _('Standardbeschreibung des Plugins'),
'type' => 'info',
'value' => $this->metadata['descriptionlong'] ?? $this->metadata['description'],
'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'de_DE'"
],
'manifest_info_en' => [
'label' => sprintf(_('Standardbeschreibung des Plugins (%s)'), _('Englisch')),
'type' => 'info',
'value' => $this->metadata['descriptionlong_en'] ?? $this->metadata['description_en'],
'if' => "STUDIPFORM_SELECTEDLANGUAGES.description === 'en_GB'"
],
'decription_mode' => [
'label' => _('Modus der neuen Beschreibung'),
'type' => 'select',
'options' => [
'add' => _('Hinzufügen zur Standardbeschreibung'),
'override_description' => _('Standardbeschreibung überschreiben'),
'replace_all' => _('Beschreibungsfenster komplett ersetzen durch Beschreibung')
]
],
'highlight_until' => [
'label' => _('In Veranstaltungen bewerben bis (oder leer lassen)'),
'type' => 'datetimepicker'
],
'highlight_text' => [
'label' => _('Bewerbungs-Infotext')
]
]
])->autoStore()
//->setDebugMode(true)
->setURL(URLHelper::getURL('dispatch.php/admin/plugin/index'));
}
}
......@@ -401,7 +401,7 @@ class Admin_SemesterController extends AuthenticatedController
FROM `semester_courses`
JOIN `semester_data` USING (`semester_id`)
GROUP BY `course_id`
HAVING MAX(`beginn`) <= ?";
HAVING MAX(`beginn`) = ?";
$course_ids = DBManager::get()->fetchFirst($query, [$semester->beginn]);
// Leave early if no courses are affected
......
<?php
class Admin_TreeController extends AuthenticatedController
{
public function rangetree_action()
{
$GLOBALS['perm']->check('root');
Navigation::activateItem('/admin/locations/range_tree');
PageLayout::setTitle(_('Einrichtungshierarchie bearbeiten'));
$this->startId = Request::get('node_id', 'RangeTreeNode_root');
$this->semester = Request::option('semester', Semester::findCurrent()->id);
$this->classname = RangeTreeNode::class;
$this->setupSidebar();
}
public function semtree_action()
{
$GLOBALS['perm']->check('root');
Navigation::activateItem('/admin/locations/sem_tree');
PageLayout::setTitle(_('Veranstaltungshierarchie bearbeiten'));
$this->startId = Request::get('node_id', 'StudipStudyArea_root');
$this->semester = Request::option('semester', Semester::findCurrent()->id);
$this->classname = StudipStudyArea::class;
$this->setupSidebar();
}
/**
* Edit the given node.
*
* @param string $class_id concatenated classname and node id
* @return void
*/
public function edit_action(string $class_id)
{
$GLOBALS['perm']->check('root');
PageLayout::setTitle(_('Eintrag bearbeiten'));
$data = $this->checkClassAndId($class_id);
$this->node = $data['classname']::getNode($data['id']);
$parent = $data['classname']::getNode($this->node->parent_id);
$this->treesearch = QuickSearch::get(
'parent_id',
new TreeSearch($data['classname'] === StudipStudyArea::class ? 'sem_tree_id' : 'range_tree_id')
)->withButton();
$this->treesearch->defaultValue($parent->id, $parent->getName());
if ($data['classname'] === RangeTreeNode::class) {
$this->instsearch = QuickSearch::get(
'studip_object_id',
new StandardSearch('Institut_id')
)->withButton();
if ($this->node->studip_object_id) {
$this->instsearch->defaultValue($this->node->studip_object_id, $this->node->institute->name);
}
}
$this->from = Request::get('from');
}
/**
* Create a new child node of the given parent.
*
* @param string $class_id concatenated classname and parent id
* @return void
*/
public function create_action(string $class_id)
{
$GLOBALS['perm']->check('root');
PageLayout::setTitle(_('Neuen Eintrag anlegen'));
$data = $this->checkClassAndId($class_id);
$this->node = new $data['classname']();
$this->node->parent_id = $data['id'];
$parent = $data['classname']::getNode($data['id']);
$this->treesearch = QuickSearch::get(
'parent_id',
new TreeSearch(get_class($this->node) === StudipStudyArea::class ? 'sem_tree_id' : 'range_tree_id')
)->withButton();
$this->treesearch->defaultValue($parent->id, $parent->getName());
$this->instsearch = QuickSearch::get(
'studip_object_id',
new StandardSearch('Institut_id')
)->withButton();
$this->from = Request::get('from');
}
/**
* Delete the given child node.
*
* @param string $class_id concatenated classname and node id
* @return void
*/
public function delete_action(string $class_id)
{
$GLOBALS['perm']->check('root');
$data = $this->checkClassAndId($class_id);
if (!Request::isPost()) {
throw new MethodNotAllowedException();
}
$node = $data['classname']::getNode($data['id']);
if ($node) {
$node->delete();
} else {
$this->set_status(404);
}
$this->render_nothing();
}
/**
* Store the given node.
*
* @param string $classname
* @param string $node_id
* @return void
*/
public function store_action(string $classname, string $node_id = '')
{
$GLOBALS['perm']->check('root');
CSRFProtection::verifyUnsafeRequest();
$node = new $classname($node_id);
$node->parent_id = Request::option('parent_id');
$parent = $classname::getNode(Request::option('parent_id'));
$maxprio = max(array_map(
function ($c) {
return $c->priority;
},
$parent->getChildNodes()
));
$node->priority = $maxprio + 1;
if (Request::option('studip_object_id')) {
$node->studip_object_id = Request::option('studip_object_id');
$node->name = '';
} else {
$node->name = Request::get('name');
}
if ($classname === StudipStudyArea::class) {
$node->info = Request::get('description');
$node->type = Request::int('type');
}
if ($node->store() !== false) {
Pagelayout::postSuccess(_('Die Daten wurden gespeichert.'));
} else {
Pagelayout::postError(_('Die Daten konnten nicht gespeichert werden.'));
}
$this->relocate(Request::get('from'));
}
public function sort_action($parent_id)
{
$GLOBALS['perm']->check('root');
$data = $this->checkClassAndId($parent_id);
$parent = $data['classname']::getNode($data['id']);
$children = $parent->getChildNodes();
$data = json_decode(Request::get('sorting'), true);
foreach ($children as $child) {
$child->priority = $data[$child->id];
$child->store();
}
$this->render_nothing();
}
/**
* (De-)assign several courses at once to a sem_tree node
* @return void
* @throws Exception
*/
public function batch_assign_semtree_action()
{
$GLOBALS['perm']->check('admin');
//set the page title with the area of Stud.IP:
PageLayout::setTitle(_('Veranstaltungszuordnungen bearbeiten'));
Navigation::activateItem('/browse/my_courses/list');
$GLOBALS['perm']->check('admin');
// check the assign_semtree array and extract the relevant course IDs:
$courseIds = Request::optionArray('assign_semtree');
$order = Config::get()->IMPORTANT_SEMNUMBER
? "ORDER BY `start_time` DESC, `VeranstaltungsNummer`, `Name`"
: "ORDER BY `start_time` DESC, `Name`";
$this->courses = Course::findMany($courseIds, $order);
$this->return = Request::get('return');
// check if at least one course was selected (this can only happen from admin courses overview):
if (!$courseIds) {
PageLayout::postWarning('Es wurde keine Veranstaltung gewählt.');
$this->relocate('admin/courses');
}
}
public function assign_courses_action($class_id)
{
$GLOBALS['perm']->check('root');
$data = $this->checkClassAndId($class_id);
$GLOBALS['perm']->check('admin');
$this->search = QuickSearch::get('courses[]', new StandardSearch('Seminar_id'))->withButton();
$this->node = $data['id'];
}
/**
* Store (de-)assignments from courses to sem_tree nodes.
* @return void
*/
public function do_batch_assign_action()
{
$GLOBALS['perm']->check('admin');
$astmt = DBManager::get()->prepare("INSERT IGNORE INTO `seminar_sem_tree` VALUES (:course, :node)");
$dstmt = DBManager::get()->prepare(
"DELETE FROM `seminar_sem_tree` WHERE `seminar_id` IN (:courses) AND `sem_tree_id` = :node");
$success = true;
// Add course assignments to the specified nodes.
foreach (Request::optionArray('courses') as $course) {
foreach (Request::optionArray('add_assignments') as $a) {
$success = $astmt->execute(['course' => $course, 'node' => $a]);
}
}
// Remove course assignments from the specified nodes.
foreach (Request::optionArray('delete_assignments') as $d) {
$success = $dstmt->execute(['courses' => Request::optionArray('courses'), 'node' => $d]);
}
if ($success) {
PageLayout::postSuccess(_('Die Zuordnungen wurden gespeichert.'));
} else {
PageLayout::postError(_('Die Zuordnungen konnten nicht vollständig gespeichert werden.'));
}
$this->relocate(Request::get('return', 'admin/courses'));
}
private function setupSidebar()
{
$sidebar = Sidebar::Get();
$semWidget = new SemesterSelectorWidget($this->url_for(''), 'semester');
$semWidget->includeAll(true);
$semWidget->setId('semester-selector');
$semWidget->setSelection($this->semester);
$sidebar->addWidget($semWidget);
if ($this->classname === StudipStudyArea::class) {
$sidebar->addWidget(new VueWidget('assign-widget'));
}
}
/**
* CHeck a combination of class name and ID for validity: is this a StudipTreeNode subclass?
* If yes, return the corresponding object.
*
* @param string $class_id class name and ID, separated by '_'
* @return mixed
*/
private function checkClassAndId($class_id)
{
list($classname, $id) = explode('_', $class_id);
if (is_a($classname, StudipTreeNode::class, true)) {
return [
'classname' => $classname,
'id' => $id
];
}
throw new InvalidArgumentException(
sprintf('The given class "%s" does not implement the StudipTreeNode interface!', $classname)
);
}
}
......@@ -440,6 +440,8 @@ class Admin_UserController extends AuthenticatedController
if (
$GLOBALS['perm']->have_perm('root')
&& Config::get()->ALLOW_ADMIN_USERACCESS
&& !StudipAuthAbstract::CheckField('auth_user_md5.password', $this->user->auth_plugin)
&& $this->user->auth_plugin !== null
&& (Request::get('pass_1') !== '' || Request::get('pass_2') !== '')
) {
if (Request::get('pass_1') === Request::get('pass_2')) {
......@@ -889,16 +891,14 @@ class Admin_UserController extends AuthenticatedController
}
if ($GLOBALS['perm']->have_perm('root')) {
$sql
= "SELECT Institut_id, Name, 1 AS is_fak
$sql = "SELECT Institut_id, Name, 1 AS is_fak
FROM Institute
WHERE Institut_id=fakultaets_id
ORDER BY Name";
$faks = DBManager::get()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
$domains = UserDomain::getUserDomains();
} else {
$sql
= "SELECT a.Institut_id, Name, b.Institut_id = b.fakultaets_id AS is_fak
$sql = "SELECT a.Institut_id, Name, b.Institut_id = b.fakultaets_id AS is_fak
FROM user_inst a
LEFT JOIN Institute b USING (Institut_id)
WHERE a.user_id = ? AND a.inst_perms = 'admin'
......@@ -909,8 +909,7 @@ class Admin_UserController extends AuthenticatedController
$domains = UserDomain::getUserDomainsForUser(User::findCurrent()->id);
}
$query
= "SELECT Institut_id, Name
$query = "SELECT Institut_id, Name
FROM Institute
WHERE fakultaets_id = ? AND institut_id != fakultaets_id
ORDER BY Name";
......
......@@ -15,20 +15,16 @@ class BlubberController extends AuthenticatedController
public function index_action($thread_id = null)
{
Navigation::activateItem('/community/blubber');
$this->threads = BlubberThread::findMyGlobalThreads(
51,
null,
null,
null,
Request::get("search")
);
if (Navigation::hasItem('/community/blubber')) {
Navigation::activateItem('/community/blubber');
}
$this->search = Request::get('search');
$this->threads = BlubberThread::findMyGlobalThreads(21, null, null, null, $this->search);
if (count($this->threads) > 20) {
array_pop($this->threads);
$this->threads_more_down = 1;
}
if ($thread_id) {
$GLOBALS['user']->cfg->store('BLUBBER_DEFAULT_THREAD', $thread_id);
} else {
......@@ -47,14 +43,8 @@ class BlubberController extends AuthenticatedController
$this->thread = array_pop($threads);
}
$this->thread_data = [];
if ($this->thread) {
$this->thread->markAsRead();
$this->thread_data = $this->thread->getJSONData(
50,
null,
Request::get("search")
);
}
if (
......@@ -62,19 +52,20 @@ class BlubberController extends AuthenticatedController
&& !Avatar::getAvatar($GLOBALS['user']->id)->is_customized()
) {
$_SESSION['already_asked_for_avatar'] = true;
PageLayout::postInfo(sprintf(
_('Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s.'),
'<a href="' . URLHelper::getLink("dispatch.php/avatar/update/user/" . $GLOBALS['user']->id) . '" data-dialog>',
'</a>'
));
}
if (Request::isDialog()) {
PageLayout::setTitle($this->thread->getName());
PageLayout::postInfo(
sprintf(
_('Wollen Sie ein Avatar-Bild nutzen? %sLaden Sie jetzt ein Bild hoch%s.'),
'<a href="' .
URLHelper::getLink('dispatch.php/avatar/update/user/' . $GLOBALS['user']->id) .
'" data-dialog>',
'</a>'
)
);
}
$this->buildSidebar();
if (Request::isDialog()) {
PageLayout::setTitle($this->thread->getName());
$this->render_template('blubber/dialog');
}
}
......@@ -111,7 +102,7 @@ class BlubberController extends AuthenticatedController
$statement = DBManager::get()->prepare($query);
$statement->execute([
'me' => $GLOBALS['user']->id,
'friend' => $user_ids[0]
'friend' => $user_ids[0],
]);
$thread_id = $statement->fetchColumn();
if ($thread_id) {
......@@ -141,16 +132,19 @@ class BlubberController extends AuthenticatedController
foreach ($user_ids as $user_id) {
$insert->execute([
'thread_id' => $blubber->getId(),
'user_id' => $user_id,
'user_id' => $user_id,
]);
}
$this->redirect("blubber/index/{$blubber->getId()}");
return;
}
$this->contacts = Contact::findBySQL("JOIN auth_user_md5 USING (user_id) WHERE owner_id = ? ORDER BY auth_user_md5.Nachname ASC, auth_user_md5.Vorname ASC", [
$GLOBALS['user']->id
]);
$this->contacts = Contact::findBySQL(
"JOIN auth_user_md5 USING (user_id)
WHERE owner_id = ?
ORDER BY auth_user_md5.Nachname, auth_user_md5.Vorname",
[$GLOBALS['user']->id]
);
}
public function delete_action($thread_id)
......@@ -164,7 +158,7 @@ class BlubberController extends AuthenticatedController
$this->thread->delete();
PageLayout::postSuccess(_('Der Blubber wurde gelöscht.'));
}
$this->redirect("blubber/index");
$this->redirect('blubber/index');
return;
}
......@@ -194,7 +188,7 @@ class BlubberController extends AuthenticatedController
LIMIT 1";
$statement = DBManager::get()->prepare($query);
$statement->execute([
'me' => $GLOBALS['user']->id,
'me' => $GLOBALS['user']->id,
'friend' => $user_ids[0],
]);
$thread_id = $statement->fetchColumn();
......@@ -225,7 +219,7 @@ class BlubberController extends AuthenticatedController
foreach ($user_ids as $user_id) {
$insert->execute([
'thread_id' => $blubber->getId(),
'user_id' => $user_id,
'user_id' => $user_id,
]);
}
$this->redirect("blubber/index/{$blubber->getId()}");
......@@ -265,8 +259,12 @@ class BlubberController extends AuthenticatedController
{
$context = Request::get('context', $GLOBALS['user']->id);
$context_type = Request::option('context_type');
if (!Request::isPost()
|| ($context_type === 'course' && !$GLOBALS['perm']->have_studip_perm('autor', $context))
if (
!Request::isPost()
|| (
$context_type === 'course'
&& !$GLOBALS['perm']->have_studip_perm('autor', $context)
)
) {
throw new AccessDeniedException();
}
......@@ -276,7 +274,6 @@ class BlubberController extends AuthenticatedController
$newfile = null; //is filled below
$file_ref = null; //is also filled below
if ($file['size']) {
$document['user_id'] = $GLOBALS['user']->id;
$document['filesize'] = $file['size'];
......@@ -290,11 +287,10 @@ class BlubberController extends AuthenticatedController
AND data_content = :content",
[
'parent_id' => $root_dir->getId(),
'content' => json_encode(['Blubber']),
'content' => json_encode(['Blubber']),
]
);
if ($blubber_directory) {
$blubber_directory = $blubber_directory->getTypedFolder();
} else {
......@@ -321,10 +317,10 @@ class BlubberController extends AuthenticatedController
$uploaded = FileManager::handleFileUpload(
[
'tmp_name' => [$file['tmp_name']],
'name' => [$file['name']],
'size' => [$file['size']],
'type' => [$file['type']],
'error' => [$file['error']]
'name' => [$file['name']],
'size' => [$file['size']],
'type' => [$file['type']],
'error' => [$file['error']],
],
$blubber_directory,
$GLOBALS['user']->id
......@@ -332,7 +328,7 @@ class BlubberController extends AuthenticatedController
if ($uploaded['error']) {
throw new Exception(implode("\n", $uploaded['error']));
} elseif($uploaded['files'][0]) {
} elseif ($uploaded['files'][0]) {
$oldbase = URLHelper::setBaseURL($GLOBALS['ABSOLUTE_URI_STUDIP']);
$url = $uploaded['files'][0]->getDownloadURL();
URLHelper::setBaseURL($oldbase);
......@@ -340,14 +336,12 @@ class BlubberController extends AuthenticatedController
} else {
throw new Exception('File cannot be created!');
}
}
} catch (Exception $e) {
$output['errors'][] = $e->getMessage();
$success = false;
}
if ($success) {
$type = null;
......@@ -387,12 +381,9 @@ class BlubberController extends AuthenticatedController
$statement = DBManager::get()->prepare($query);
$statement->execute([
'thread_id' => $thread_id,
'user_id' => Request::option('user_id'),
'user_id' => Request::option('user_id'),
]);
$this->response->add_header(
'X-Dialog-Execute',
'STUDIP.Blubber.refreshThread'
);
$this->response->add_header('X-Dialog-Execute', 'STUDIP.Blubber.refreshThread');
$this->response->add_header('X-Dialog-Close', '1');
$this->render_json([
'thread_id' => $thread_id,
......@@ -405,7 +396,7 @@ class BlubberController extends AuthenticatedController
if ($this->thread['context_type'] !== 'private' || !$this->thread->isReadable()) {
throw new AccessDeniedException();
}
PageLayout::setTitle(_("Studiengruppe aus Konversation erstellen"));
PageLayout::setTitle(_('Studiengruppe aus Konversation erstellen'));
if (Request::isPost() && count(studygroup_sem_types())) {
$studgroup_sem_types = studygroup_sem_types();
$course = new Course();
......@@ -436,7 +427,9 @@ class BlubberController extends AuthenticatedController
$this->thread->store();
PluginManager::getInstance()->setPluginActivated(
PluginManager::getInstance()->getPlugin('Blubber')->getPluginId(),
PluginManager::getInstance()
->getPlugin('Blubber')
->getPluginId(),
$course->getId(),
true
);
......@@ -451,62 +444,47 @@ class BlubberController extends AuthenticatedController
if ($this->thread['context_type'] !== 'private' || !$this->thread->isReadable()) {
throw new AccessDeniedException();
}
PageLayout::setTitle(_("Private Konversation verlassen"));
PageLayout::setTitle(_('Private Konversation verlassen'));
if (Request::isPost()) {
BlubberMention::deleteBySQL("user_id = :me AND external_contact = '0' AND thread_id = :thread_id", [
'thread_id' => $this->thread->getId(),
'me' => $GLOBALS['user']->id
'me' => $GLOBALS['user']->id,
]);
if (Request::get("delete_comments")) {
if (Request::get('delete_comments')) {
BlubberComment::deleteBySQL("thread_id = :thread_id AND user_id = :me AND external_contact = '0'", [
'thread_id' => $this->thread->getId(),
'me' => $GLOBALS['user']->id
'me' => $GLOBALS['user']->id,
]);
}
if ($this->thread['user_id'] === $GLOBALS['user']->id) {
$this->thread['content'] = "";
$this->thread['content'] = '';
$this->thread->store();
}
$count_departed = BlubberMention::countBySQL("INNER JOIN auth_user_md5 USING (user_id) WHERE external_contact = '0' AND thread_id = :thread_id", [
'thread_id' => $this->thread->getId()
]);
$count_departed = BlubberMention::countBySQL(
"JOIN auth_user_md5 USING (user_id)
WHERE external_contact = 0 AND thread_id = :thread_id",
[
'thread_id' => $this->thread->getId(),
]
);
$count_comments = BlubberComment::countBySQL("thread_id = :thread_id AND external_contact = '0'", [
'thread_id' => $this->thread->getId()
'thread_id' => $this->thread->getId(),
]);
if (!$count_departed || (!$count_comments && !$this->thread['content'])) {
//ich mache das Licht aus:
$this->thread->delete();
PageLayout::postSuccess(_("Private Konversation gelöscht."));
PageLayout::postSuccess(_('Private Konversation gelöscht.'));
} else {
PageLayout::postSuccess(_("Private Konversation verlassen."));
PageLayout::postSuccess(_('Private Konversation verlassen.'));
}
$this->redirect("blubber/index");
$this->redirect('blubber/index');
}
}
protected function buildSidebar()
{
$search = new SearchWidget("#");
$search->addNeedle(
_("Suche nach ..."),
"search",
true
);
Sidebar::Get()->addWidget($search, "blubbersearch");
$threads_widget = Sidebar::Get()->addWidget(
new BlubberThreadsWidget(),
'threads'
);
foreach ($this->threads as $thread) {
$threads_widget->addThread($thread);
}
if ($this->thread) {
$threads_widget->setActive($this->thread->getId());
}
$threads_widget->withComposer();
$sidebar = Sidebar::Get();
$sidebar->addWidget(new VueWidget('blubber-search-widget'));
$sidebar->addWidget(new VueWidget('blubber-threads-widget'));
}
}
......@@ -79,6 +79,10 @@ abstract class ConsultationController extends AuthenticatedController
$block = ConsultationBlock::find($block_id);
if (!$block || !$block->range) {
throw new Exception(_('Dieser Terminblock ist ungültig.'));
}
if (!$block->range->isAccessibleToUser()) {
throw new AccessDeniedException();
}
......
......@@ -290,7 +290,8 @@ class Course_BasicdataController extends AuthenticatedController
}
//Daten sammeln:
$sem = Seminar::getInstance($this->course_id);
$course = Course::find($this->course_id);
$sem = new Seminar($course);
$data = $sem->getData();
//Erster, zweiter und vierter Reiter des Akkordions: Grundeinstellungen
......@@ -365,10 +366,51 @@ class Course_BasicdataController extends AuthenticatedController
$widget = new ActionsWidget();
$sem_create_perm = in_array(Config::get()->SEM_CREATE_PERM, ['root','admin','dozent']) ? Config::get()->SEM_CREATE_PERM : 'dozent';
if ($GLOBALS['perm']->have_perm($sem_create_perm)) {
if (!LockRules::check(Context::getId(), 'seminar_copy')) {
$widget->addLink(
_('Veranstaltung kopieren'),
$this->url_for(
'course/wizard/copy/' . $this->course_id,
['studip_ticket' => Seminar_Session::get_ticket()]
),
Icon::create('seminar')
);
}
}
$widget->addLink(_('Bild ändern'),
$this->url_for('avatar/update/course', $course_id),
Icon::create('edit')
$this->url_for('avatar/update/course', $this->course_id),
Icon::create('edit')
);
if ($GLOBALS['perm']->have_perm('admin')) {
$is_locked = $course->lock_rule;
$widget->addLink(
_('Sperrebene ändern') . ' (' . ($is_locked ? _('gesperrt') : _('nicht gesperrt')) . ')',
$this->url_for(
'course/management/lock',
['studip_ticket' => Seminar_Session::get_ticket()]
),
Icon::create('lock-' . ($is_locked ? 'locked' : 'unlocked'))
)->asDialog('size=auto');
}
if (
(Config::get()->ALLOW_DOZENT_VISIBILITY || $GLOBALS['perm']->have_perm('admin'))
&& !LockRules::Check($this->course_id, 'seminar_visibility')
) {
$is_visible = $course->visible;
if ($course->isOpenEnded() || $course->end_semester->visible) {
$widget->addLink(
$is_visible ? _('Veranstaltung verstecken') : _('Veranstaltung sichtbar schalten'),
$this->url_for(
'course/management/change_visibility',
['studip_ticket' => Seminar_Session::get_ticket()]
),
Icon::create('visibility-' . ($is_visible ? 'visible' : 'invisible'))
);
}
}
if ($this->deputies_enabled) {
if (Deputy::isDeputy($GLOBALS['user']->id, $this->course_id)) {
......@@ -388,6 +430,16 @@ class Course_BasicdataController extends AuthenticatedController
);
}
}
if (Config::get()->ALLOW_DOZENT_DELETE || $GLOBALS['perm']->have_perm('admin')) {
$widget->addLink(
_('Veranstaltung löschen'),
$this->url_for(
'course/archive/confirm',
['studip_ticket' => Seminar_Session::get_ticket()]
),
Icon::create('trash')
)->asDialog('size=auto');
}
$sidebar->addWidget($widget);
if ($GLOBALS['perm']->have_studip_perm('admin', $this->course_id)) {
$widget = new CourseManagementSelectWidget();
......@@ -524,7 +576,14 @@ class Course_BasicdataController extends AuthenticatedController
}
$this->flash['msg'] = $this->msg;
$this->flash['open'] = Request::get("open");
$this->redirect($this->url_for('course/basicdata/view/' . $sem->getId()));
if (Request::isDialog()) {
$this->response->add_header('X-Dialog-Close', 1);
$this->response->add_header('X-Dialog-Execute', 'STUDIP.AdminCourses.App.loadCourse');
$this->render_text($course_id);
} else {
$this->redirect($this->url_for('course/basicdata/view/' . $sem->getId()));
}
}
public function add_member_action($course_id, $status = 'dozent')
......
......@@ -48,6 +48,6 @@ class Course_ChangeViewController extends AuthenticatedController
public function reset_changed_view_action()
{
unset($_SESSION["seminar_change_view_{$this->course_id}"]);
$this->relocate('course/management');
$this->relocate('course/contentmodules');
}
}
<?php
class Course_ContentmodulesController extends AuthenticatedController
{
public function index_action()
{
Navigation::activateItem('/course/admin/contentmodules');
PageLayout::setTitle(_('Werkzeuge'));
if (Context::isCourse()) {
$this->sem = Context::get();
$this->sem_class = $this->sem->getSemClass();
} else {
$this->sem = Context::get();
$this->sem_class = SemClass::getDefaultInstituteClass($this->sem['type']);
}
$this->modules = $this->getModules($this->sem);
$this->highlighted_modules = [];
foreach ($this->modules as $module) {
if ($module['highlighted']) {
$this->highlighted_modules[] = $module['id'];
}
}
if (Context::isCourse()) {
$actions = new ActionsWidget();
$actions->addLink(
_('Studierendenansicht simulieren'),
URLHelper::getURL('dispatch.php/course/change_view/set_changed_view'),
Icon::create('visibility-invisible')
);
Sidebar::Get()->addWidget($actions);
}
$views = Sidebar::Get()->addWidget(new ViewsWidget());
$views->id = 'tool-view-switch';
$views->addLink(
_('Kachelansicht'),
'#tiles'
)->setActive($GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY);
$views->addLink(
_('Tabellarische Ansicht'),
'#tabular'
)->setActive(!$GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY);
$this->categories = [];
foreach ($this->modules as $i => $module) {
if ($module['category'] && !in_array($module['category'], $this->categories)) {
$this->categories[] = $module['category'];
}
if (!$module['category']) {
if (!in_array(_('Sonstige'), $this->categories)) {
$this->categories[] = _('Sonstige');
}
$this->modules[$i]['category'] = _('Sonstige');
}
}
sort($this->categories);
$filter_widget = Sidebar::Get()->addWidget(new OptionsWidget());
$filter_widget->id = 'tool-filter-category';
$filter_widget->setTitle(_('Filter nach Kategorie'));
$filter_widget->addRadioButton(
_('Alle Kategorien'),
'#',
true
);
foreach ($this->categories as $category) {
$filter_widget->addRadioButton(
$category,
'#'
);
}
if (
Context::isCourse()
&& $GLOBALS['perm']->have_studip_perm('admin', Context::getId())
&& !$this->sem_class['studygroup_mode']
) {
$widget = new CourseManagementSelectWidget();
Sidebar::Get()->addWidget($widget);
}
PageLayout::addHeadElement('script', [
'type' => 'text/javascript',
], sprintf(
'window.ContentModulesStoreData = %s;',
json_encode([
'setCategories' => $this->categories,
'setHighlighted' => $this->highlighted_modules,
'setModules' => array_values($this->modules),
'setUserId' => User::findCurrent()->id,
'setView' => $GLOBALS['user']->cfg->CONTENTMODULES_TILED_DISPLAY ? 'tiles' : 'table',
])
));
}
public function trigger_action()
{
$context = Context::get();
$required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin';
if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) {
throw new AccessDeniedException();
}
if (Request::isPost()) {
if ($context->getRangeType() === 'course') {
$sem_class = $context->getSemClass();
} else {
$sem_class = SemClass::getDefaultInstituteClass($context->type);
}
$moduleclass = Request::get('moduleclass');
$active = Request::bool('active', false);
$module = new $moduleclass;
if ($module->isActivatableForContext($context)) {
PluginManager::getInstance()->setPluginActivated($module->getPluginId(), $context->getId(), $active);
}
if ($active) {
$active_tool = ToolActivation::find([$context->id, $module->getPluginId()]);
$default_position = array_search(get_class($module), $sem_class->getActivatedModules());
if ($default_position !== false && $active_tool) {
$active_tool->position = $default_position;
$active_tool->store();
}
}
//$this->redirect("course/contentmodules/trigger", ['cid' => $context->getId()]);
}
$template = $GLOBALS['template_factory']->open('tabs.php');
$template->navigation = Navigation::getItem('/course');
Navigation::getItem('/course/admin')->setActive(true);
$this->render_json([
'tabs' => $template->render(),
'position' => $active_tool->position
]);
}
public function reorder_action()
{
$context = Context::get();
$required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin';
if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) {
throw new AccessDeniedException();
}
if (Request::isPost()) {
$position = 0;
foreach (Request::getArray('order') as $plugin_id) {
$tool = ToolActivation::find([$context->getId(), $plugin_id]);
$tool->position = $position++;
$tool->store();
}
$this->redirect($this->reorderURL());
return;
}
Navigation::getItem('/course/admin')->setActive(true);
$template = $GLOBALS['template_factory']->open('tabs.php');
$template->navigation = Navigation::getItem('/course');
$this->render_json([
'tabs' => $template->render()
]);
}
public function change_visibility_action()
{
if (!Request::isPost()) {
throw new AccessDeniedException();
}
$context = Context::get();
$required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin';
if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) {
throw new AccessDeniedException();
}
$moduleclass = Request::get('moduleclass');
$module = new $moduleclass;
$active_tool = ToolActivation::find([$context->id, $module->getPluginId()]);
$metadata = $active_tool->metadata->getArrayCopy();
if (Request::bool('visible')) {
unset($metadata['visibility']);
} else {
$metadata['visibility'] = 'tutor';
}
$active_tool['metadata'] = $metadata;
$active_tool->store();
$this->render_json([
'visibility' => $active_tool->getVisibilityPermission()
]);
}
public function tiles_display_action()
{
if (Request::isPost()) {
$GLOBALS['user']->cfg->store(
'CONTENTMODULES_TILED_DISPLAY',
Request::get('view') === 'tiles'
);
}
$this->render_nothing();
}
public function rename_action($module_id)
{
$context = Context::get();
$required_perm = $context->getRangeType() === 'course' ? 'tutor' : 'admin';
if (!$GLOBALS['perm']->have_studip_perm($required_perm, $context->id)) {
throw new AccessDeniedException();
}
$this->module = PluginManager::getInstance()->getPluginById($module_id);
$this->metadata = $this->module->getMetadata();
PageLayout::setTitle(_('Werkzeug umbenennen'));
$this->tool = ToolActivation::find([$context->id, $module_id]);
if (Request::isPost()) {
$metadata = $this->tool->metadata->getArrayCopy();
if (!trim(Request::get('displayname')) || Request::submitted('delete')) {
unset($metadata['displayname']);
} else {
$metadata['displayname'] = trim(Request::get('displayname'));
}
$this->tool['metadata'] = $metadata;
$this->tool->store();
$this->redirect('course/contentmodules/index');
}
}
public function info_action($plugin_id)
{
$this->plugin = PluginManager::getInstance()->getPluginById($plugin_id);
$this->metadata = $this->plugin->getMetadata();
PageLayout::setTitle(sprintf(_('Informationen über %s'), $this->metadata['displayname']));
}
private function getModules(Range $context)
{
$list = [];
foreach (PluginEngine::getPlugins('StudipModule') as $plugin) {
if (!$plugin->isActivatableForContext($context)) {
continue;
}
if (!$this->sem_class->isModuleAllowed(get_class($plugin))) {
continue;
}
$info = $plugin->getMetadata();
$plugin_id = $plugin->getPluginId();
$tool = ToolActivation::find([$context->getRangeId(), $plugin->getPluginId()]);
$toolname = $info['displayname'] ?? $plugin->getPluginname();
if ($tool && $tool->metadata['displayname']) {
$displayname = $tool->getDisplayname() . ' (' . $toolname . ')';
} else {
$displayname = $toolname;
}
$visibility = $tool ? $tool->getVisibilityPermission() : 'nobody';
$metadata = $plugin->getMetadata();
$list[$plugin_id] = [
'id' => $plugin_id,
'moduleclass' => get_class($plugin),
'position' => $tool ? $tool->position : null,
'toolname' => $toolname,
'displayname' => $displayname,
'visibility' => $visibility,
'active' => (bool) $tool,
];
if ($metadata['icon_clickable']) {
$list[$plugin_id]['icon'] = $metadata['icon_clickable'] instanceof Icon
? $metadata['icon_clickable']->asImagePath()
: Icon::create($plugin->getPluginURL().'/'.$metadata['icon_clickable'])->asImagePath();
} elseif ($metadata['icon']) {
$list[$plugin_id]['icon'] = $metadata['icon'] instanceof Icon
? $metadata['icon']->asImagePath()
: Icon::create($plugin->getPluginURL().'/'.$metadata['icon'])->asImagePath();
} else {
$list[$plugin_id]['icon'] = null;
}
$list[$plugin_id]['summary'] = $metadata['summary'];
$list[$plugin_id]['mandatory'] = $this->sem_class->isModuleMandatory(get_class($plugin));
$list[$plugin_id]['highlighted'] = (bool) $plugin->isHighlighted();
$list[$plugin_id]['highlight_text'] = $plugin->getHighlightText();
$list[$plugin_id]['category'] = $metadata['category'];
}
return $list;
}
}
......@@ -91,6 +91,190 @@ class Course_CoursewareController extends CoursewareController
$this->render_pdf($element->pdfExport($user, $with_children), trim($element->title).'.pdf');
}
public function comments_overview_action(): void
{
Navigation::activateItem('course/courseware/comments');
$this->setCommentsOverviewSidebar();
}
public function comments_overview_data_action()
{
$user = User::findCurrent();
$cid = Request::get('cid');
$units = [];
$elements = [];
$containers = [];
$blocks = [];
$block_comments = [];
$block_feedbacks = [];
$element_comments = [];
$element_feedbacks = [];
$statement = DBManager::get()->prepare("
SELECT elem.id AS elem_id, container.id AS container_id, block.id AS block_id, comment.id AS comment_id
FROM `cw_block_comments` AS comment
INNER JOIN `cw_blocks` AS block ON (block.id = comment.block_id)
INNER JOIN `cw_containers` AS container ON (container.id = block.container_id)
INNER JOIN `cw_structural_elements` AS elem ON (elem.id = container.structural_element_id)
WHERE elem.range_type = 'course'
AND elem.range_id = :range_id
");
$statement->execute(['range_id' => $cid]);
$cw_block_comments = $statement->fetchAll();
foreach ($cw_block_comments as $row) {
$element = \Courseware\StructuralElement::find($row['elem_id']);
if (!$element->canRead($user)) {
continue;
}
if (!$this->arrayHasDataForId($elements, $row['elem_id'])) {
$elements[] = $element;
$unit = $element->findUnit();
$unitElement = $unit->structural_element;
if (!$this->arrayHasDataForId($elements, $unitElement->id)) {
$elements[] = $unitElement;
}
if (!$this->arrayHasDataForId($units, $unit->id)) {
$units[] = $unit;
}
}
if (!$this->arrayHasDataForId($containers, $row['container_id'])) {
$containers[] = \Courseware\Container::find($row['container_id']);
}
if (!$this->arrayHasDataForId($blocks, $row['block_id'])) {
$blocks[] = \Courseware\Block::find($row['block_id']);
}
if (!$this->arrayHasDataForId($block_comments, $row['comment_id'])) {
$block_comments[] = \Courseware\BlockComment::find($row['comment_id']);
}
}
$statement = DBManager::get()->prepare("
SELECT elem.id AS elem_id, container.id AS container_id, block.id AS block_id, feedback.id AS feedback_id
FROM `cw_block_feedbacks` AS feedback
INNER JOIN `cw_blocks` AS block ON (block.id = feedback.block_id)
INNER JOIN `cw_containers` AS container ON (container.id = block.container_id)
INNER JOIN `cw_structural_elements` AS elem ON (elem.id = container.structural_element_id)
WHERE elem.range_type = 'course'
AND elem.range_id = :range_id
");
$statement->execute(['range_id' => $cid]);
$cw_block_feedbacks = $statement->fetchAll();
foreach ($cw_block_feedbacks as $row) {
$element = \Courseware\StructuralElement::find($row['elem_id']);
if (!$element->canEdit($user)) {
continue;
}
if (!$this->arrayHasDataForId($elements, $row['elem_id'])) {
$elements[] = $element;
$unit = $element->findUnit();
$unitElement = $unit->structural_element;
if (!$this->arrayHasDataForId($elements, $unitElement->id)) {
$elements[] = $unitElement;
}
if (!$this->arrayHasDataForId($units, $unit->id)) {
$units[] = $unit;
}
}
if (!$this->arrayHasDataForId($containers, $row['container_id'])) {
$containers[] = \Courseware\Container::find($row['container_id']);
}
if (!$this->arrayHasDataForId($blocks, $row['block_id'])) {
$blocks[] = \Courseware\Block::find($row['block_id']);
}
if (!$this->arrayHasDataForId($block_feedbacks, $row['feedback_id'])) {
$block_feedbacks[] = \Courseware\BlockFeedback::find($row['feedback_id']);
}
}
$statement = DBManager::get()->prepare("
SELECT elem.id AS elem_id, comment.id AS comment_id
FROM `cw_structural_element_comments` AS comment
INNER JOIN `cw_structural_elements` AS elem ON (elem.id = comment.structural_element_id)
WHERE elem.range_type = 'course'
AND elem.range_id = :range_id
");
$statement->execute(['range_id' => $cid]);
$cw_structural_element_comments = $statement->fetchAll();
foreach ($cw_structural_element_comments as $row) {
$element = \Courseware\StructuralElement::find($row['elem_id']);
if (!$element->canRead($user)) {
continue;
}
if (!$this->arrayHasDataForId($elements, $row['elem_id'])) {
$elements[] = $element;
$unit = $element->findUnit();
$unitElement = $unit->structural_element;
if (!$this->arrayHasDataForId($elements, $unitElement->id)) {
$elements[] = $unitElement;
}
if (!$this->arrayHasDataForId($units, $unit->id)) {
$units[] = $unit;
}
}
if (!$this->arrayHasDataForId($element_comments, $row['comment_id'])) {
$element_comments[] = \Courseware\StructuralElementComment::find($row['comment_id']);
}
}
$statement = DBManager::get()->prepare("
SELECT elem.id AS elem_id, feedback.id AS feedback_id
FROM `cw_structural_element_feedbacks` AS feedback
INNER JOIN `cw_structural_elements` AS elem ON (elem.id = feedback.structural_element_id)
WHERE elem.range_type = 'course'
AND elem.range_id = :range_id
");
$statement->execute(['range_id' => $cid]);
$cw_structural_element_feedbacks = $statement->fetchAll();
foreach ($cw_structural_element_feedbacks as $row) {
$element = \Courseware\StructuralElement::find($row['elem_id']);
if (!$element->canEdit($user)) {
continue;
}
if (!$this->arrayHasDataForId($elements, $row['elem_id'])) {
$elements[] = $element;
$unit = $element->findUnit();
$unitElement = $unit->structural_element;
if (!$this->arrayHasDataForId($elements, $unitElement->id)) {
$elements[] = $unitElement;
}
if (!$this->arrayHasDataForId($units, $unit->id)) {
$units[] = $unit;
}
}
if (!$this->arrayHasDataForId($element_feedbacks, $row['feedback_id'])) {
$element_feedbacks[] = \Courseware\StructuralElementFeedback::find($row['feedback_id']);
}
}
$encoder = app(\Neomerx\JsonApi\Contracts\Encoder\EncoderInterface::class);
$data = [
'units' => $encoder->encodeData($units),
'elements' => $encoder->encodeData($elements),
'containers' => $encoder->encodeData($containers),
'blocks' => $encoder->encodeData($blocks),
'block_comments' => $encoder->encodeData($block_comments),
'block_feedbacks' => $encoder->encodeData($block_feedbacks),
'element_comments' => $encoder->encodeData($element_comments),
'element_feedbacks' => $encoder->encodeData($element_feedbacks),
];
$this->render_json($data);
}
private function arrayHasDataForId(array $array, $id): bool
{
$ids = array_column($array, null, 'id');
return !empty($ids[$id]);
}
private function setIndexSidebar(): void
{
$sidebar = Sidebar::Get();
......@@ -112,4 +296,12 @@ class Course_CoursewareController extends CoursewareController
$sidebar->addWidget(new VueWidget('courseware-activities-widget-filter-type'));
$sidebar->addWidget(new VueWidget('courseware-activities-widget-filter-unit'));
}
private function setCommentsOverviewSidebar(): void
{
$sidebar = Sidebar::Get();
$sidebar->addWidget(new VueWidget('courseware-comments-overview-widget-filter-type'));
$sidebar->addWidget(new VueWidget('courseware-comments-overview-widget-filter-created'));
$sidebar->addWidget(new VueWidget('courseware-comments-overview-widget-filter-unit'));
}
}