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 (1071)
Showing
with 1451 additions and 417 deletions
......@@ -20,3 +20,6 @@ block_comment_start = /*
block_comment_end = */
line_comment = //
quote_type = single
[*.yml]
indent_size = 2
\ No newline at end of file
......@@ -3,6 +3,18 @@
# MYSQL_PASSWORD=""
# MYSQL_DATABASE=""
# Enable the next line to display the debug bar in development mode
# DEBUG_BAR=1
# Enable the following to allow opening files from exception displays in your
# editor. Beware: You need to provide a full path for prefix your files since
# the exception only displays the relative path.
#
# Variables being substituted: %{file} and %{line}
#
# EDITOR_URL="phpstorm://open?file=<path-to-your-studip>/%{file}&line=%{line}"
# EDITOR_URL="vscode://file/<path-to-your-studip>/%{file}:%{line}:0
# STUDIP_CACHING_ENABLE=""
# STUDIP_CACHE_IS_SESSION_STORAGE=""
# STUDIP_ENV=""
......
{
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:vue/essential",
"@vue/eslint-config-typescript"
],
"globals": {
"STUDIP": "writable",
"CKEDITOR": "writable",
"$": "writable",
"_": "writable",
"jQuery": "writable"
},
"plugins": [
"vue"
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-async-promise-executor": "error",
"no-await-in-loop": "error",
"no-promise-executor-return": "error",
"require-atomic-updates": "error",
"max-nested-callbacks": ["error", 4],
"no-return-await": "error",
"prefer-promise-reject-errors": "error",
"vue/multi-word-component-names": "off"
}
}
......@@ -23,6 +23,7 @@ data/oer_logos/*
data/upload_doc/*
public/.htaccess
public/.rnd
public/assets/javascripts/*.js
public/assets/javascripts/*.js.map
public/assets/stylesheets/*.css
......
......@@ -17,7 +17,7 @@ variables:
# Use faster docker driver
DOCKER_DRIVER: overlay2
# Images
NODE_IMAGE: node:16-slim
NODE_IMAGE: node:18-slim
# Directories
CACHE_DIR: .caches
REPORT_DIR: .reports
......@@ -29,6 +29,7 @@ stages:
- checks
- analyse
- test
- cache
- packaging
- release
......@@ -159,17 +160,11 @@ lint-js:
interruptible: true
before_script:
- *mkdir-reports
- npm install -g npm@7
- npm install
--no-save --no-audit --no-fund
--loglevel=error
eslint eslint-formatter-gitlab
- npm ci --prefer--offline
script:
- npx eslint
--ext .js,.vue
- npm run lint-js --
--cache --cache-location $CACHE_LOCATION
--format gitlab
resources/assets/javascripts resources/vue
artifacts:
reports:
codequality: $ESLINT_CODE_QUALITY_REPORT
......@@ -183,24 +178,20 @@ lint-css:
STYLELINT_CODE_QUALITY_REPORT: $REPORT_DIR/stylelint-codequality.json
interruptible: true
cache:
- key: "$CI_JOB_NAME_SLUG:CI_COMMIT_REF_SLUG"
- key: "CI_COMMIT_REF_SLUG-lint-css"
paths:
- $CACHE_LOCATION
policy: pull-push
when: always
before_script:
- *mkdir-caches
- *mkdir-reports
- npm install
--no-save --no-audit --no-fund
--loglevel=error
stylelint@^14.9.1
stylelint-config-standard@^26.0.0
stylelint-formatter-gitlab
script:
- npx
stylelint
- npm run lint-css --
--cache --cache-location $CACHE_LOCATION
--custom-formatter=node_modules/stylelint-formatter-gitlab
--custom-formatter=node_modules/stylelint-formatter-gitlab
--output-file $STYLELINT_CODE_QUALITY_REPORT
resources/assets/stylesheets
artifacts:
reports:
codequality: $STYLELINT_CODE_QUALITY_REPORT
......@@ -211,9 +202,7 @@ phpstan:
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
- key: "$CO_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
......@@ -223,12 +212,12 @@ phpstan:
- *mkdir-caches
- *mkdir-reports
- *install-composer
- 'echo "includes:\n - phpstan.neon.dist\n\nparameters:\n tmpDir: $PHPSTAN_CACHE_PATH" > phpstan.neon'
- 'echo -e "includes:\n - phpstan.neon.dist\n\nparameters:\n tmpDir: $PHPSTAN_CACHE_PATH" > phpstan.neon'
script:
- php
composer/bin/phpstan analyse
--memory-limit=1G
--no-progress
composer/bin/phpstan analyse
--memory-limit=1G
--no-progress
--level=$PHPSTAN_LEVEL
--error-format=gitlab > $PHPSTAN_CODE_QUALITY_REPORT
after_script:
......@@ -414,7 +403,7 @@ packaging:
expire_in: never
build_image:
image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
stage: build
......
{
"extends": "stylelint-config-standard",
"extends": "stylelint-config-standard-scss",
"rules": {
"alpha-value-notation": null,
"at-rule-empty-line-before": null,
"at-rule-no-vendor-prefix": null,
"block-closing-brace-empty-line-before": null,
"block-closing-brace-newline-after": null,
"block-closing-brace-newline-before": null,
"block-closing-brace-space-before": null,
"block-no-empty": null,
"block-opening-brace-newline-after": null,
"block-opening-brace-space-after": null,
"block-opening-brace-space-before": null,
"color-function-notation": null,
"color-hex-case": null,
"color-hex-length": null,
"color-no-invalid-hex": null,
"comment-empty-line-before": null,
"comment-whitespace-inside": null,
"declaration-bang-space-after": null,
"declaration-bang-space-before": null,
"custom-property-empty-line-before": null,
"declaration-block-no-duplicate-properties": null,
"declaration-block-no-redundant-longhand-properties": null,
"declaration-block-no-shorthand-property-overrides": null,
"declaration-block-semicolon-newline-after": null,
"declaration-block-semicolon-space-before": null,
"declaration-block-single-line-max-declarations": null,
"declaration-block-trailing-semicolon": null,
"declaration-colon-newline-after": null,
"declaration-colon-space-after": null,
"declaration-colon-space-before": null,
"declaration-empty-line-before": null,
"font-family-name-quotes": null,
"font-family-no-missing-generic-family-keyword": null,
"function-comma-space-after": null,
"function-comma-space-before": null,
"function-name-case": null,
"function-no-unknown": null,
"function-parentheses-newline-inside": null,
"function-parentheses-space-inside": null,
"function-url-quotes": null,
"hue-degree-notation": null,
"indentation": null,
"import-notation": "string",
"keyframes-name-pattern": null,
"length-zero-no-unit": null,
"max-empty-lines": null,
"max-line-length": null,
"media-feature-range-notation": "prefix",
"media-feature-name-no-vendor-prefix": null,
"no-descending-specificity": null,
"no-duplicate-selectors": null,
"no-empty-first-line": null,
"no-empty-source": null,
"no-eol-whitespace": null,
"no-extra-semicolons": null,
"no-invalid-position-at-import-rule": null,
"no-irregular-whitespace": null,
"no-missing-end-of-source-newline": null,
"number-leading-zero": null,
"number-max-precision": null,
"number-no-trailing-zeros": null,
"property-no-vendor-prefix": null,
"rule-empty-line-before": null,
"selector-attribute-quotes": null,
"selector-class-pattern": null,
"selector-combinator-space-after": null,
"selector-combinator-space-before": null,
"selector-descendant-combinator-no-non-space": null,
"selector-id-pattern": null,
"selector-list-comma-newline-after": null,
"selector-no-vendor-prefix": null,
"selector-not-notation": null,
"selector-pseudo-element-colon-notation": null,
"shorthand-property-no-redundant-values": null,
"string-quotes": null,
"value-keyword-case": null,
"value-list-max-empty-lines": null,
"value-no-vendor-prefix": null
"value-no-vendor-prefix": null,
"custom-property-pattern": [
"^([a-z][a-z0-9]*-+)*[a-z0-9]+$",
{"message": "Expected custom property name to be kebab-case-ish (multiple dashes are allowed"}
],
"scss/at-extend-no-missing-placeholder": null,
"scss/at-mixin-pattern": null,
"scss/dollar-variable-empty-line-before": null,
"scss/dollar-variable-pattern": null,
"scss/double-slash-comment-empty-line-before": null,
"scss/function-unquote-no-unquoted-strings-inside": null,
"scss/no-global-function-names": null
},
"overrides": [
{
"files": ["**/*.scss"],
"customSyntax": "postcss-scss",
"rules": {
"at-rule-no-unknown": null
}
}
],
"ignoreFiles": [
"resource/assets/stylesheets/jquery-ui.structure.css",
"resources/assets/stylesheets/vendor/*"
......
This diff is collapsed.
......@@ -38,7 +38,7 @@ PROJECT_NAME = Stud.IP
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 5.4
PROJECT_NUMBER = 6.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
......
......@@ -87,12 +87,12 @@ optimize-icons: npm
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
find public/assets/images/icons/blue -type f -print0 | xargs -0 -n1 -I{} echo 'sed "s/#28497c/#000000/g" {} > {}' | 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/g" {} > {}' | 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/g" {} > {}' | 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/g" {} > {}' | 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/g" {} > {}' | 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/g" {} > {}' | sed 's#icons/blue#icons/yellow#2' | sh
# default rules for gettext handling
js-%.pot: $(VUE_SOURCES)
......
......@@ -4,10 +4,77 @@
## Neue Features
### System
- Der Stud.IP-Cache ist nun kompatibel zu PSR-6. ([TIC #3701](https://gitlab.studip.de/studip/studip/-/issues/3701))
- Das `User`-Model hat die Methode `hasPermissionLevel()` erhalten, um einfach abfragen zu können, ob eine Person einen bestimmten Berechtigungsstatus hat. ([Issue #3453](https://gitlab.studip.de/studip/studip/-/issues/3453))
- In der Standort-Verwaltung können nun nicht nur Ferien sondern auch Feiertage konfiguriert werden. Dies erlaubt das Markieren von Feiertagen als gesetzliche Feiertage, da diese je nach Bundesland variieren können. ([Issue #2795](https://gitlab.studip.de/studip/studip/-/issues/2795))
- Die Nutzungsbedingungen sind nun nicht mehr als statische HTML-Dateien hinterlegt, sondern können analog zu Impressum, Datenschutz- und Barrierefreiheitserklärung direkt über die Oberfläche bearbeitet werden. Initial ist diese Seite aber im Entwurfsmodus und daher für Nicht-Roots unsichtbar. Damit andere Personen beim ersten Login diese Nutzungsbedingungen sehen und ihnen zustimmen können, muss der Entwurfsmodus für diese Seite abgeschaltet werden. ([TIC #4433](https://gitlab.studip.de/studip/studip/-/issues/4433))
### Layout
- Die Login-Seite wurde überarbeitet
- Das Design von Stud.IP wurde modernisiert
- Neue Avatarbilder
### Courseware
- Rechte und Sichtbarkeit überarbeitet
- Verbesserter Übersichtsdialog
- Rechte und Sichtbarkeit können für ein ganzes Lernmaterial gesetzt werden
- [Issue #3442](https://gitlab.studip.de/studip/studip/-/issues/3442)
- Peer-Review für Aufgaben
- Lehrende können Peer-Review-Prozess für Aufgaben einstellen
- Lernende sehen Aufgabenlösungen ein, geben Feedback und Bewertung dazu ab
- [Issue #2484](https://gitlab.studip.de/studip/studip/-/issues/2484)
- Blubber-Block
- Blubber Diskussionen können jetzt als Block eingebunden werden
- Lernmaterialübersicht im Inhaltsverzeichnis
- Schafft einen Überblick über weitere Lernmaterialien in der Veranstaltungen ohne den Kontext verlassen zu müssen
- Neues Layout für den Merksatz-Block
### Vips Plugin wird als Aufgaben-Werkzeug in Kern integriert
- Mit Vips lassen sich Selbsttests, Übungen und Klausuren erstellen
- Lernende können Vips Aufgabenblätter in Stud.IP bearbeiten und erhalten dort auch ihre Ergebnisse
- Für eine Vielzahl von Aufgabentypen besteht die Möglichkeit einer Autokorrektur
- [Issue #4258](https://gitlab.studip.de/studip/studip/-/issues/4258)
- [Plugin](https://develop.studip.de/studip/plugins.php/pluginmarket/presenting/details/81097da5ef66a002998b75d5eeece1f0)
### Neues Benachrichtigungssystem
- Vereinheitlichung wie Stud.IP mit Nutzenden kommuniziert
- Messageboxen, Companionmeldungen und Notifications werden vereinheitlicht
- [Issue #660](https://gitlab.studip.de/studip/studip/-/issues/660)
### Garuda Plugin in Kern integriert
- Mit Garuda können Zielgruppen definiert werden, um diesen Sammelnachrichten zukommen zu lassen
- [Issue #3326](https://gitlab.studip.de/studip/studip/-/issues/3326)
- [Plugin](https://develop.studip.de/studip/plugins.php/pluginmarket/presenting/details/d24cb47ee246033c325496b832e64147)
### Bilderpool
- Neuer Archiv Upload
- Ermöglicht das Einpflegen von einer Vielzahl von Bilder mit nur einem Upload
- Metadaten werden in dem Archiv als CSV-Datei mitgegeben und müssen nicht mehr einzeln eingegeben werden
- Stud.IP Bildersammlung kann mit nur einem Upload integriert werden
- Die Stud.IP Bildersammlung finden Sie [hier](https://gitlab.studip.de/studip/bilderpool)
### Anzeige von ILIAS-Kursen auf "Mein Arbeitsplatz"
- Bei aktivierter ILIAS-Schnittstelle können Lehrende ihre Stud.IP Veranstaltungen mit ILIAS-Kursen verknüpfen
- Lernende erhalten eine Übersicht über ihre ILIAS-Kurse im Stud.IP Arbeitsplatz
### Assistent für Roots nach Updates
- Nach einem Update auf eine neue Stud.IP Version werden Nutzenden mit Root-Rechten die neuen Features der Version präsentiert
### Studiengruppen
- Um Studiengruppen sichtbarer und interessanter zu machen wurde eine Reihe von Verbesserungen vorgenommen
- Neues Widget für die Startseite
- Möglichkeit Studiengruppen für eine Veranstaltungen vorzuschlagen
- [Issue #3616](https://gitlab.studip.de/studip/studip/-/issues/3616)
## Breaking changes
- Mindestanforderung an PHP auf 8.1 angehoben ([TIC #3805](https://gitlab.studip.de/studip/studip/issues/3805))
- Im Rahmen von [Issue #3788](https://gitlab.studip.de/studip/studip/-/issues/3788) wurden die Zusätze an allen Icons entfernt. Dadurch kann es sein, dass manche Plugins nicht mehr erscheinen. Diese müssen dann auf eine Variante ohne Zusätze umgestellt werden.
- Die Funktion `get_config()` wurde entfernt. Stattdessen muss die Methode `Config::get()->getValue('CONFIG_KEY')` bzw. der Shortcut `Config::get()->CONFIG_KEY` verwendet werden. ([Issue #2797](https://gitlab.studip.de/studip/studip/-/issues/2797))
- Die Funktion `smile()` wurde entfernt. Sie kann ersatzlos entfernt werden. ([Issue #3158](https://gitlab.studip.de/studip/studip/-/issues/3158))
......@@ -41,6 +108,41 @@
Sollte ein Plugin manuell Flexi einbinden, so wird dies zu einem Fehler führen. Jegliches Einbinden von Dateien
unterhalb von `vendor/flexi` muss ersatzlos entfernt werden.
- Die folgenden Funktionen wurden entfernt ([Issue #4179](https://gitlab.studip.de/studip/studip/-/issues/4179))
- `getWeekdays($short = true)`
- `veranstaltung_beginn($seminar_id = '', $return_mode = '')`
- `veranstaltung_beginn_from_metadata($reg_irreg, $sem_begin, $start_woche, $start_termin,$turnus_data, $return_mode='int')`
- `get_sem_name ($time)`
- `get_sem_num ($time)`
- `get_sem_num_sem_browse ()`
- `get_semester($seminar_id)`
- `delete_date($termin_id, $topic_delete = TRUE, $folder_move = TRUE, $sem_id=0)`
- `delete_range_of_dates($range_id, $topics = FALSE)`
- `isSchedule ($sem_id, $presence_dates_only = TRUE, $clearcache = FALSE)`
- `isMetadateCorrespondingDate ($termin_id, $begin = '', $end = '', $seminar_id='')`
- `getPresenceTypes()`
- Die Klasse `AuxLockRules` wurde ausgebaut. ([Issue #4187](https://gitlab.studip.de/studip/studip/-/issues/4187))
- Die Klasse `ProfileModel` wurde gelöscht. Die darin enthaltenen Methoden wurden in den `Profile_Controller` verschoben. ([Issue #4185]https://gitlab.studip.de/studip/studip/-/issues/4185))
- Die Klasse `StudipTransformFormat` wurde ausgebaut ([Issue #4188](https://gitlab.studip.de/studip/studip/-/issues/4188))
- Die REST-API (`public/api.php`) wurde zu Stud.IP 5.0 deprecated und nun mit Stud.IP 6.0 entfernt. Als Ersatz steht die JSONAPI zur Verfügung. ([Issue #2798](https://gitlab.studip.de/studip/studip/-/issues/2798))
- Die Klassen `CalendarView`, `CalendarScheduleModel` sowie alle davon abhängigen Klassen wurden entfernt. ([Issue #4421](https://gitlab.studip.de/studip/studip/-/issues/4421))
- Anstelle von `CalendarView` sollte `\Studip\Fullcalendar` verwendet werden.
- Das Datenbankschema des Stundenplans wurde geändert. ([Issue #4421](https://gitlab.studip.de/studip/studip/-/issues/4421))
- Die Evaluationen wurden ausgebaut. Stattdessen sollte man nun die neuen Fragebögen verwenden ([Issue #3787]https://gitlab.studip.de/studip/studip/-/issues/3787)
- Die Klassen `DbView`, `DbSnapshot` und die zugehörigen Dateien in `lib/dbviews` wurden ausgebaut. ([Issue #4390](https://gitlab.studip.de/studip/studip/-/issues/4390))
- Als Ersatz dienen Datenbankabfragen mittels der `DBManager`-Klasse oder mittels `SimpleORMap`-Modellen.
- Es wurden zwei neue CLI-Kommandos hinzugefügt, womit man Klassenrümpfe für SORM-Models und Migrationen erstellen kann. Bei den Migrationen wird die Versionsnummer für die jeweilige `domain` automatisch ermittelt.
- `cli/studip make:model` und `cli/studip make:migration`.
- Es wurde ein neues CLI-Kommando hinzugefügt, womit man auf einfache Weise ein Plugin-Grundgerüst erstellen kann.
- `cli/studip make:plugin`
- Die Klasse `Seminar`, sowie die Klassen in `lib/raumzeit` wurden ausgebaut. ([Issue #3209](https://gitlab.studip.de/studip/studip/-/issues/3209))
- Als Ersatz für viele Methoden der Seminar-Klasse dienen die Klassen `Course`, `CourseDate` und `SeminarCycleDate`, sowie die neue `CourseDateList`-Klasse.
- Die Klassen `TreeAbstract`, `TreeView` und `SemBrowse` wurden ausgebaut. ([Issue #4392](https://gitlab.studip.de/studip/studip/-/issues/4392))
- Zur Anzeige von Baumstrukturen können als Ersatz die Implementierungen des `StudipTreeNode`-Interfaces genutzt werden.
- Die Zuordnung von Veranstaltungen zu Semestern anhand von Timestamps wurde entfernt. In der Datenbank wurden die Spalten `start_time` und `duration_time` der Tabelle `seminare` entfernt. ([Issue #4391]https://gitlab.studip.de/studip/studip/-/issues/4391))
- Plugins, die Veranstaltungen anhand von Timestamps laden oder anderweitig verwenden, müssen angepasst werden!
- Das Mapping von Veranstaltungen zu Semestern findet nun ausschließlich anhand der Semester-ID über die Verknüpfungstabelle `semester_courses` statt.
- Die ELearning-Schnittstelle wurde ausgebaut. In diesem Rahmen wurden auch die Methoden `printhead()` und `printcontent()` ersatzlos entfernt. ([Issue #4109](https://gitlab.studip.de/studip/studip/-/issues/4109))
## Security related issues
......
RELEASE 5.4.alpha
RELEASE 6.0.alpha
......@@ -92,4 +92,193 @@ class ActivityfeedController extends AuthenticatedController
PageLayout::setTitle(_('Aktivitäten konfigurieren'));
}
public function load_action(): void
{
$user = User::findCurrent();
// failsafe einbauen - falls es keine älteren Aktivitäten mehr im System gibt, Abbruch!
$oldest_activity = \Studip\Activity\Activity::getOldestActivity();
$max_age = $oldest_activity ? $oldest_activity->mkdate : time();
$contexts = [];
// create system context
$system_context = new \Studip\Activity\SystemContext($user);
$contexts[] = $system_context;
$contexts[] = new \Studip\Activity\UserContext($user, $user);
$user->contacts->each(function ($another_user) use (&$contexts, $user) {
$contexts[] = new \Studip\Activity\UserContext($another_user, $user);
});
if (!in_array($user->perms, ['admin','root'])) {
// create courses and institutes context
foreach (\Course::findMany($user->course_memberships->pluck('seminar_id')) as $course) {
$contexts[] = new \Studip\Activity\CourseContext($course, $user);
}
foreach (\Institute::findMany($user->institute_memberships->pluck('institut_id')) as $institute) {
$contexts[] = new \Studip\Activity\InstituteContext($institute, $user);
}
}
// add filters
$filter = new \Studip\Activity\Filter();
$start = Request::int('start', strtotime('yesterday'));
$end = Request::int('end', time());
$scrollfrom = Request::int('scrollfrom', false);
$filtertype = Request::get('filtertype', '');
$objectType = Request::get('object_type');
$filter->setObjectType($objectType);
$objectId = Request::get('object_id');
$filter->setObjectId($objectId);
$context = Request::get('context_type');
$filter->setContext($context);
$contextId = Request::get('context_id');
$filter->setContextId($contextId);
if (!empty($filtertype)) {
$filter->setType(json_decode($filtertype));
}
if ($scrollfrom) {
// shorten "watch-window" by one second to prevent duplication of activities
$scrollfrom -= 1;
if ($scrollfrom > $max_age){
$end = $scrollfrom;
$start = strtotime('yesterday', $end);
$data = [];
$backtrack = 1;
while (empty($data)) {
$filter->setStartDate($start);
$filter->setEndDate($end);
$data = $this->getStreamData($contexts, $filter);
if ($start < $max_age) {
break;
}
// move "watch-window" back one day at a time
$end = $start - 1;
$start = strtotime("-{$backtrack} days", $start);
// enforce maximum "watch-window", currently 2 weeks
$backtrack = min(14, $backtrack + 1);
}
} else {
$data = false;
}
} else {
$filter->setStartDate($start);
$filter->setEndDate($end);
$data = $this->getStreamData($contexts, $filter);
}
// set etag for preventing resending the same stuff over and over again
$etag = md5(serialize($data));
$this->response->add_header('ETag', '"' . $etag . '"');
if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $this->etagMatches($etag, $_SERVER['HTTP_IF_NONE_MATCH'])) {
$this->set_status(304);
$this->render_nothing();
return;
}
if (isset($_SERVER['HTTP_IF_MATCH']) && !$this->etagMatches($etag, $_SERVER['HTTP_IF_MATCH'])) {
$this->set_status(412);
$this->render_nothing();
return;
}
$this->render_json($data);
}
/**
* private helper function to get stream data for given contexts and filter
*
* @param $contexts
* @param $filter
* @return array
*/
private function getStreamData($contexts, $filter): array
{
$stream = new Studip\Activity\Stream($contexts, $filter);
$data = $stream->toArray();
foreach ($data as $key => $act) {
$actor = [
'type' => $act['actor_type'],
'id' => $act['actor_id'],
];
if ($act['actor_type'] == 'user') {
$a_user = \User::findFull($act['actor_id']);
$actor['details'] = $this->getMiniUser($a_user ?: new \User());
} elseif ($act['actor_type'] === 'anonymous') {
$actor['details'] = [
'name' => _('Anonym'),
];
}
unset($data[$key]['actor_type']);
unset($data[$key]['actor_id']);
$data[$key]['actor'] = $actor;
}
return $data;
}
private function getMiniUser(User $user): array
{
$avatar = \Avatar::getAvatar($user->id);
return [
'id' => $user->id,
'name' => $this->getNamesOfUser($user),
'avatar_small' => $avatar->getURL(\Avatar::SMALL),
'avatar_medium' => $avatar->getURL(\Avatar::MEDIUM),
'avatar_normal' => $avatar->getURL(\Avatar::NORMAL),
'avatar_original' => $avatar->getURL(\Avatar::NORMAL)
];
}
private function getNamesOfUser(User $user): array
{
return [
'username' => $user->username,
'formatted' => $user->getFullName(),
'family' => $user->nachname,
'given' => $user->vorname,
'prefix' => $user->title_front,
'suffix' => $user->title_rear,
];
}
// Helper method checking if a ETag value list includes the current ETag.
private function etagMatches(string $etag, string $list)
{
if ($list === '*') {
return true;
}
return in_array(
$etag,
preg_split('/\s*,\s*/', $list)
);
}
}
......@@ -31,7 +31,9 @@ class Admin_AdditionalController extends AuthenticatedController
"Veranstaltung zu verändern."));
}
Sidebar::get()->addWidget(new CourseManagementSelectWidget());
if ($GLOBALS['perm']->have_studip_perm('admin', $this->course->id)) {
Sidebar::get()->addWidget(new CourseManagementSelectWidget());
}
}
/**
......
<?php
/**
*
**/
class Admin_ApiController extends AuthenticatedController
{
/**
*
**/
public function before_filter(&$action, &$args)
{
parent::before_filter($action, $args);
require_once 'lib/bootstrap-api.php';
$GLOBALS['perm']->check('root');
Navigation::activateItem('/admin/config/api');
PageLayout::setTitle(_('API Verwaltung'));
$this->types = [
'website' => _('Website'),
'desktop' => _('Herkömmliches Desktopprogramm'),
'mobile' => _('Mobile App')
];
// Sidebar
$views = new ViewsWidget();
$views->addLink(_('Registrierte Applikationen'),
$this->url_for('admin/api'))
->setActive($action === 'index');
$views->addLink(_('Globale Zugriffseinstellungen'),
$this->url_for('admin/api/permissions'))
->setActive($action == 'permissions');
$views->addLink(_('Konfiguration'),
$this->url_for('admin/api/config'))
->setActive($action == 'config');
Sidebar::get()->addWidget($views);
$actions = new ActionsWidget();
$actions->addLink(_('Neue Applikation registrieren'),
$this->url_for('admin/api/edit'),
Icon::create('add', 'clickable'))
->asDialog();
Sidebar::get()->addWidget($actions);
}
/**
*
**/
public function index_action()
{
$this->consumers = RESTAPI\Consumer\Base::findAll();
$this->routes = RESTAPI\Router::getInstance()->getRoutes(true);
}
/**
*
**/
public function render_keys($id)
{
$consumer = RESTAPI\Consumer\Base::find($id);
return [
'Consumer Key = ' . $consumer->auth_key,
'Consumer Secret = ' . $consumer->auth_secret,
];
}
/**
*
**/
public function keys_action($id)
{
$details = $this->render_keys($id);
if (Request::isXhr()) {
$this->render_text(implode('<br>', $details));
} else {
PageLayout::postMessage(MessageBox::info(_('Die Schlüssel in den Details dieser Meldung sollten vertraulich behandelt werden!'), $details, true));
$this->redirect('admin/api/#' . $id);
}
}
/**
*
**/
public function edit_action($id = null)
{
$consumer = $id
? RESTAPI\Consumer\Base::find($id)
: RESTAPI\Consumer\Base::create(Request::option('consumer_type') ?: 'oauth');
if (Request::submitted('store')) {
$errors = [];
$consumer->active = (bool) Request::int('active');
$consumer->title = Request::get('title');
$consumer->contact = Request::get('contact');
$consumer->email = Request::get('email');
$consumer->callback = Request::get('callback');
$consumer->url = Request::get('url');
$consumer->type = Request::get('type') ?: null;
$consumer->commercial = Request::int('commercial');
$consumer->notes = Request::get('notes');
$consumer->description = Request::get('description');
if (!empty($errors)) {
$message = MessageBox::error(_('Folgende Fehler sind aufgetreten:'), $errors);
PageLayout::postMessage($message);
return;
}
$consumer->store();
if ($id) {
$message = MessageBox::success(_('Die Applikation wurde erfolgreich gespeichert.'));
} else {
$details = $this->render_keys($consumer->id);
$message = MessageBox::success(_('Die Applikation wurde erfolgreich erstellt, die Schlüssel finden Sie in den Details dieser Meldung.'), $details, true);
}
PageLayout::postMessage($message);
$this->redirect('admin/api/index#' . $consumer->id);
return;
}
$this->consumer = $consumer;
$this->id = $id;
}
/**
*
**/
public function toggle_action($id, $state = null)
{
$consumer = RESTAPI\Consumer\Base::find($id);
$consumer->active = $state === null ? !$consumer->active : ($state === 'on');
$consumer->store();
$message = $state
? _('Die Applikation wurde erfolgreich aktiviert.')
: _('Die Applikation wurde erfolgreich deaktiviert.');
PageLayout::postMessage(MessageBox::success($message));
$this->redirect('admin/api/#' . $consumer->id);
}
/**
*
**/
public function delete_action($id)
{
if (!Request::isPost()) {
throw new MethodNotAllowedException();
}
if ($consumer = RESTAPI\Consumer\Base::find($id)) {
$consumer->delete();
PageLayout::postSuccess(_('Die Applikation wurde erfolgreich gelöscht.'));
}
$this->redirect('admin/api');
}
/**
*
**/
public function permissions_action($consumer_id = null)
{
if (Request::submitted('store')) {
$perms = Request::getArray('permission');
$permissions = RESTAPI\ConsumerPermissions::get($consumer_id ?: 'global');
foreach ($perms as $route => $methods) {
foreach ($methods as $method => $granted) {
$permissions->set(urldecode($route), urldecode($method), (bool)$granted, true);
}
}
$permissions->store();
PageLayout::postMessage(MessageBox::success(_('Die Zugriffsberechtigungen wurden erfolgreich gespeichert')));
$this->redirect($consumer_id ? 'admin/api' : 'admin/api/permissions');
return;
}
$title = $consumer_id ? _('Zugriffsberechtigungen') : _('Globale Zugriffsberechtigungen');
$title .= ' - ' . PageLayout::getTitle();
PageLayout::setTitle($title);
$this->consumer_id = $consumer_id;
$this->router = RESTAPI\Router::getInstance();
$this->routes = $this->router->getRoutes(true, false);
$this->permissions = RESTAPI\ConsumerPermissions::get($consumer_id ?: 'global');
$this->global = $consumer_id ? RESTAPI\ConsumerPermissions::get('global') : false;
}
public function config_action()
{
$this->config = Config::get();
if (Request::isPost()) {
$this->config->store('API_ENABLED', Request::int('active', 0));
$this->config->store('API_OAUTH_AUTH_PLUGIN', Request::option('auth'));
PageLayout::postMessage(MessageBox::success(_('Die Einstellungen wurden gespeichert.')));
$this->redirect('admin/api/config');
}
}
}
......@@ -44,11 +44,11 @@ class Admin_AutoinsertController extends AuthenticatedController
if (Request::submitted('suchen')) {
if (Request::get('sem_search')) {
$this->sem_search = Request::get('sem_search');
$this->sem_select = Request::option('sem_select');
$this->sem_select = Request::option('sem_select') ?: null;
$search = new SeminarSearch();
$this->seminar_search = $search->getResults
(Request::get('sem_search'),
['search_sem_sem' => Request::option('sem_select')]
$this->seminar_search = $search->getResults(
$this->sem_search,
['search_sem_sem' => $this->sem_select]
);
if (count($this->seminar_search) == 0) {
PageLayout::postInfo(_('Es wurden keine Veranstaltungen gefunden.'));
......@@ -159,7 +159,7 @@ class Admin_AutoinsertController extends AuthenticatedController
} elseif (!count($filters)) {
PageLayout::postError(_('Keine Filterkriterien gewählt'));
} else {
$seminar = Seminar::GetInstance($seminar_id);
$course = Course::find($seminar_id);
$userlookup = new UserLookup();
foreach ($filters as $type => $values) {
......@@ -170,8 +170,11 @@ class Admin_AutoinsertController extends AuthenticatedController
foreach ($user_ids as $user_id) {
if ($force || !AutoInsert::checkAutoInsertUser($seminar_id, $user_id)) {
$real_users += $seminar->addMember($user_id) ? 1 : 0;
AutoInsert::saveAutoInsertUser($seminar_id, $user_id);
$user = User::find($user_id);
if ($user) {
$real_users += $course->addMember($user) ? 1 : 0;
AutoInsert::saveAutoInsertUser($seminar_id, $user_id);
}
}
}
......@@ -182,9 +185,8 @@ class Admin_AutoinsertController extends AuthenticatedController
count($user_ids),
sprintf(
'<a href="%s">%s</a>',
URLHelper::getLink('dispatch.php/course/details/', ['cid' => $seminar->getId()]),
htmlReady($seminar->getName()
)
URLHelper::getLink('dispatch.php/course/details/', ['cid' => $course->id]),
htmlReady($course->name)
)
);
$details = [_('Etwaige Abweichungen der Personenzahlen enstehen durch bereits vorhandene bzw. wieder ausgetragene Personen.')];
......
......@@ -156,9 +156,7 @@ class Admin_BannerController extends AuthenticatedController
}
break;
case 'seminar':
try {
Seminar::getInstance($target);
} catch (Exception $e) {
if (!Course::exists($target)) {
$errors[] = _('Die angegebene Veranstaltung existiert nicht. '
.'Bitte geben Sie eine gültige Veranstaltungs-ID ein.');
}
......@@ -199,14 +197,14 @@ class Admin_BannerController extends AuthenticatedController
->defaultValue($banner->target,$seminar_name['name'])
->render();
}
if ($banner->target_type == 'user') {
$this->user = QuickSearch::get('user', new StandardSearch('username'))
->setInputStyle('width: 240px')
->defaultValue($banner->target, $banner->target)
->render();
}
if ($banner->target_type == 'inst') {
$institut_name = get_object_name($banner->target, 'inst');
$this->institut = QuickSearch::get('institut', new StandardSearch('Institut_id'))
......
......@@ -47,16 +47,14 @@ class Admin_CacheController extends AuthenticatedController
$this->sidebar->addWidget($views);
if ($this->enabled) {
$actions = new ActionsWidget();
$actions->addLink(
_('Cache leeren'),
$this->url_for('admin/cache/flush'),
Icon::create('decline'),
['data-confirm' => _('Soll der gesamte Inhalt des Caches wirklich gelöscht werden?')]
);
$this->sidebar->addWidget($actions);
}
$actions = new ActionsWidget();
$actions->addLink(
_('Cache leeren'),
$this->url_for('admin/cache/flush'),
Icon::create('decline'),
['data-confirm' => _('Soll der gesamte Inhalt des Caches wirklich gelöscht werden?')]
);
$this->sidebar->addWidget($actions);
}
/**
......@@ -64,28 +62,29 @@ class Admin_CacheController extends AuthenticatedController
*/
public function settings_action()
{
if ($this->enabled) {
$this->types = CacheType::findAndMapBySQL(function (CacheType $type) {
return $type->toArray();
}, "1 ORDER BY `cache_id`");
$currentCache = Config::get()->SYSTEMCACHE;
$currentCacheClass = CacheType::findOneByClass_name($currentCache['type']);
$this->cache = $currentCacheClass->class_name;
$this->config = $currentCacheClass->class_name::getConfig();
} else {
PageLayout::postWarning(
_('Caching ist systemweit ausgeschaltet, daher kann hier nichts konfiguriert werden.'));
}
$currentCache = Config::get()->SYSTEMCACHE;
$currentCacheClass = CacheType::findOneByClass_name($currentCache['type']);
$this->render_vue_app(
Studip\VueApp::create('CacheAdministration')
->withProps([
'enabled' => (bool) $this->enabled,
'currentCache' => $currentCacheClass->class_name,
'currentConfig' => $currentCacheClass->class_name::getConfig(),
'cacheTypes' => CacheType::findAndMapBySQL(
fn(CacheType $type) => $type->toArray(),
"1 ORDER BY `cache_id`"
),
])
);
}
/**
* Fetches necessary configuration for given cache type.
*
* @param string $className
*/
public function get_config_action($className)
public function get_config_action()
{
$className = Request::get('cache');
$type = CacheType::findOneByClass_name($className);
$this->render_json($type->class_name::getConfig());
......@@ -122,7 +121,7 @@ class Admin_CacheController extends AuthenticatedController
*/
public function flush_action()
{
$cache = \Studip\Cache\Factory::getCache();
$cache = \Studip\Cache\Factory::loadSystemCache(true);
$cache->flush();
PageLayout::postSuccess(_('Die Inhalte des Caches wurden gelöscht.'));
......
......@@ -12,10 +12,12 @@ class Admin_CourseplanningController extends AuthenticatedController
{
parent::before_filter($action, $args);
if ($GLOBALS['perm']->have_perm('admin')) {
Navigation::activateItem('/browse/my_courses/schedule');
if (!$GLOBALS['perm']->have_perm('admin')) {
throw new AccessDeniedException();
}
Navigation::activateItem('/browse/my_courses/schedule');
$this->insts = Institute::getMyInstitutes($GLOBALS['user']->id);
if (empty($this->insts) && !$GLOBALS['perm']->have_perm('root')) {
......@@ -82,7 +84,7 @@ class Admin_CourseplanningController extends AuthenticatedController
foreach ($this->events as $event) {
$start_date_time = explode('T', $event['start']);
$time_elements = explode(':', $start_date_time[1]);
if (!$event['conform'] || $time_elements[0] % 2) {
if (empty($event['conform']) || $time_elements[0] % 2) {
Sidebar::get()->getWidget('actions')->addLink(
_('Veranstaltungen außerhalb des Rasters'),
$this->nonconformURL(),
......
......@@ -23,7 +23,6 @@
* @since 3.1
*/
require_once 'lib/meine_seminare_func.inc.php';
require_once 'lib/object.inc.php';
require_once 'lib/archiv.inc.php'; //for lastActivity in getCourses() method
......@@ -171,11 +170,16 @@ class Admin_CoursesController extends AuthenticatedController
Now draw the configurable elements according
to the values inside the visibleElements array.
*/
$institute_id = null;
if (!empty($visibleElements['search'])) {
$this->setSearchWiget();
}
if (!empty($visibleElements['institute'])) {
$filter->addElement($this->getInstSelector());
$inst_selector = $filter->addElement($this->getInstSelector());
if (count($inst_selector->getOptions()) === 1) {
$institute_id = $this->insts[0]['Institut_id'];
}
}
if (!empty($visibleElements['semester'])) {
$filter->addElement($this->getSemesterSelector());
......@@ -187,7 +191,7 @@ class Admin_CoursesController extends AuthenticatedController
$filter->addElement($this->getCourseTypeWidget());
}
if (!empty($visibleElements['teacher'])) {
$filter->addElement($this->getTeacherWidget());
$filter->addElement($this->getTeacherWidget($institute_id));
}
$sidebar->addWidget($filter, 'filter');
......@@ -293,8 +297,7 @@ class Admin_CoursesController extends AuthenticatedController
PageLayout::setHelpKeyword('Basis.Veranstaltungen');
PageLayout::setTitle(_('Verwaltung von Veranstaltungen und Einrichtungen'));
// Add admission functions.
PageLayout::addScript('studip-admission.js');
$this->max_show_courses = 500;
$this->max_show_courses = Config::get()->MAX_SHOW_ADMIN_COURSES;
}
/**
......@@ -305,15 +308,9 @@ class Admin_CoursesController extends AuthenticatedController
$this->fields = $this->getViewFilters();
$this->sortby = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT ?? (Config::get()->IMPORTANT_SEMNUMBER ? 'number' : 'name');
$this->sortflag = $GLOBALS['user']->cfg->MEINE_SEMINARE_SORT_FLAG ?? 'ASC';
$this->store_data = $this->getStoreData();
$this->buildSidebar();
PageLayout::addHeadElement('script', [
'type' => 'text/javascript',
], sprintf(
'window.AdminCoursesStoreData = %s;',
json_encode($this->getStoreData())
));
}
private function getStoreData(): array
......@@ -430,7 +427,6 @@ class Admin_CoursesController extends AuthenticatedController
break;
case 8: //Sperrebenen
$template = $tf->open('admin/courses/lock_preselect');
$template->course = $course;
$template->all_lock_rules = new SimpleCollection(array_merge(
[[
'name' => '--' . _('keine Sperrebene') . '--',
......@@ -447,7 +443,6 @@ class Admin_CoursesController extends AuthenticatedController
break;
case 10: //Zusatzangaben
$template = $tf->open('admin/courses/aux_preselect');
$template->course = $course;
$template->aux_lock_rules = AuxLockRule::findBySQL('1 ORDER BY name ASC');
$data['buttons_top'] = $template->render();
$data['buttons_bottom'] = (string) \Studip\Button::createAccept(_('Zusatzangaben'), 'aux_button', ['formaction' => URLHelper::getURL('dispatch.php/admin/courses/set_aux_lockrule')]);
......@@ -486,7 +481,17 @@ class Admin_CoursesController extends AuthenticatedController
$data['buttons_bottom'] = (string) \Studip\Button::createAccept(
_('Teilnehmendenexport'), 'batch_export_members',
[
'formaction' => URLHelper::getURL('dispatch.php/admin/user/batch_export_members'),
'formaction' => URLHelper::getURL('dispatch.php/admin/courses/batch_export_members'),
'data-dialog' => 'size=big'
]);
break;
case 23: // Mass mail to selected courses
$data['buttons_top'] = '<label>' . _('Alle auswählen') .
'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>';
$data['buttons_bottom'] = (string) \Studip\Button::createAccept(
_('Nachricht an ausgewählte Veranstaltungen'), 'massmail',
[
'formaction' => URLHelper::getURL('dispatch.php/massmail/quick/courses'),
'data-dialog' => 'size=big'
]);
break;
......@@ -496,7 +501,7 @@ class Admin_CoursesController extends AuthenticatedController
$multimode = $plugin->useMultimode();
if ($multimode) {
$data['buttons_top'] = '<label>'._('Alle auswählen').'<input type="checkbox" data-proxyfor=".course-admin td:last-child :checkbox"></label>';
if ($multimode instanceof Flex\Template) {
if ($multimode instanceof Flexi\Template) {
$data['buttons_bottom'] = $multimode->render();
} elseif ($multimode instanceof \Studip\Button) {
$data['buttons_bottom'] = (string) $multimode;
......@@ -556,7 +561,9 @@ class Admin_CoursesController extends AuthenticatedController
if (!$config->MY_INSTITUTES_DEFAULT) {
$config->delete('ADMIN_COURSES_TEACHERFILTER');
} else {
$exists = InstituteMember::countBySQL("INNER JOIN `auth_user_md5` USING (`user_id`) WHERE `user_inst`.`user_id` = :user_id AND `user_inst`.`Institut_id` = :institut_id AND `auth_user_md5`.`perms` = 'dozent' ", [
$include_children = $GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN ? ' OR Institute.fakultaets_id = :institut_id ' : '';
$exists = InstituteMember::countBySQL("INNER JOIN `Institute` USING (`Institut_id`) WHERE `user_inst`.`user_id` = :user_id AND (`Institute`.`Institut_id` = :institut_id $include_children) AND `user_inst`.`inst_perms` = 'dozent' ", [
'user_id' => $config->ADMIN_COURSES_TEACHERFILTER,
'institut_id' => $config->MY_INSTITUTES_DEFAULT
]) > 0;
......@@ -632,7 +639,7 @@ class Admin_CoursesController extends AuthenticatedController
}
if (in_array('number', $activated_fields)) {
$d['number'] = '<a href="'.URLHelper::getLink('dispatch.php/course/basicdata/view', ['cid' => $course->id]).'">'
.$course->veranstaltungsnummer
. htmlReady($course->veranstaltungsnummer)
.'</a>';
}
if (in_array('avatar', $activated_fields)) {
......@@ -642,20 +649,18 @@ class Admin_CoursesController extends AuthenticatedController
}
if (in_array('type', $activated_fields)) {
$semtype = $course->getSemType();
$d['type'] = $semtype['name'];
$d['type'] = htmlReady($semtype['name']);
}
if (in_array('room_time', $activated_fields)) {
$seminar = new Seminar($course);
$d['room_time'] = $seminar->getDatesHTML([
'show_room' => true,
]) ?: _('nicht angegeben');
$strings = $course->getAllDatesInSemester()->toStringArray();
$d['room_time'] = implode('<br>', $strings) ?: _('nicht angegeben');
}
if (in_array('semester', $activated_fields)) {
$d['semester'] = $course->semester_text;
$d['semester'] = htmlReady($course->semester_text);
$d['semester_sort'] = $course->start_semester ? $course->start_semester->beginn : 0;
}
if (in_array('institute', $activated_fields)) {
$d['institute'] = $course->home_institut ? $course->home_institut->name : $course->institute;
$d['institute'] = htmlReady($course->home_institut ? $course->home_institut->name : $course->institute);
}
if (in_array('requests', $activated_fields)) {
$d['requests'] = '<a href="'.URLHelper::getLink('dispatch.php/course/room_requests', ['cid' => $course->id]).'">'.count($course->room_requests)."</a>";
......@@ -699,8 +704,8 @@ class Admin_CoursesController extends AuthenticatedController
foreach ($icons as $icon) {
$d['contents'] .= '<li class="my-courses-navigation-item '. ($icon->getImage()->signalsAttention() ? 'my-courses-navigation-important' : '').'">
<a href="'. URLHelper::getLink('seminar_main.php', ['auswahl' => $course->id, 'redirect_to' => $icon->getURL()]).'"'. ($icon->getTitle() ? ' title="'.htmlReady($icon->getTitle()).'"' : '') .'>
'. $icon->getImage()->asImg(20) .'
<a href="'. URLHelper::getLink('dispatch.php/course/go', ['to' => $course->id, 'redirect_to' => $icon->getURL()]).'"'. ($icon->getTitle() ? ' title="'.htmlReady($icon->getTitle()).'"' : '') .'>
'. $icon->getImage()->asImg() .'
</a>
</li>';
}
......@@ -836,7 +841,12 @@ class Admin_CoursesController extends AuthenticatedController
$d['action'] = $template->render();
break;
case 22: //Masssenexport Teilnehmendendaten
$template = $tf->open('admin/courses/batch_export_members');
$template = $tf->open('admin/courses/export_members');
$template->course = $course;
$d['action'] = $template->render();
break;
case 23: //Masssenexport Teilnehmendendaten
$template = $tf->open('admin/courses/massmail');
$template->course = $course;
$d['action'] = $template->render();
break;
......@@ -962,6 +972,7 @@ class Admin_CoursesController extends AuthenticatedController
if (count($filter_config) > 0) {
$filter = AdminCourseFilter::get();
PluginEngine::sendMessage(AdminCourseWidgetPlugin::class, 'applyFilters', $filter);
$filter->query->orderBy('seminare.name');
$courses = $filter->getCourses();
$view_filters = $this->getViewFilters();
......@@ -969,7 +980,6 @@ class Admin_CoursesController extends AuthenticatedController
$data = [];
foreach ($courses as $course) {
$sem = new Seminar($course);
$row = [];
if (in_array('number', $filter_config)) {
......@@ -989,11 +999,9 @@ class Admin_CoursesController extends AuthenticatedController
}
if (in_array('room_time', $filter_config)) {
$_room = $sem->getDatesExport([
'semester_id' => $this->semester->id,
'show_room' => true
]);
$row['room_time'] = $_room ?: _('nicht angegeben');
$dates = $course->getAllDatesInSemester($this->semester);
$date_strings = $dates->toStringArray(true);
$row['room_time'] = implode("\n", $date_strings) ?: _('nicht angegeben');
}
if (in_array('requests', $filter_config)) {
......@@ -1191,7 +1199,7 @@ class Admin_CoursesController extends AuthenticatedController
$course = Course::find($course_id);
if ($course->isOpenEnded() || $course->end_semester->visible) {
$visibility = $visibilites[$course_id] ?: 0;
$visibility = $visibilites[$course_id] ?? 0;
if ($course->visible == $visibility) {
continue;
......@@ -1322,6 +1330,73 @@ class Admin_CoursesController extends AuthenticatedController
$this->notice = $course->config->COURSE_ADMIN_NOTICE;
}
public function batch_export_members_action()
{
PageLayout::setTitle(_('Teilnehmendendaten exportieren'));
$courseIds = Request::optionArray('export_members');
$order = Config::get()->IMPORTANT_SEMNUMBER
? "ORDER BY `VeranstaltungsNummer`, `Name`"
: "ORDER BY `Name`";
$this->courses = Course::findMany($courseIds, $order);
// check if at least one course was selected (this can only happen from admin courses overview):
if (count($this->courses) === 0) {
PageLayout::postWarning('Es wurde keine Veranstaltung gewählt.');
$this->relocate('admin/courses');
}
}
/*
* Export member data of all selected courses
*/
public function do_batch_export_action()
{
if (Request::submitted('xlsx')) {
$export_format = 'xlsx';
} else if (Request::submitted('csv')) {
$export_format = 'csv';
} else {
PageLayout::postError('Nicht unterstütztes Exportformat.');
$this->relocate('admin/courses');
}
$tmp_folder = $GLOBALS['TMP_PATH'] . '/temp_folder_' . md5(uniqid());
mkdir($tmp_folder);
$courses = Course::findMany(Request::optionArray('courses'));
$header = [
_('Status'),
_('Anrede'),
_('Titel'),
_('Vorname'),
_('Nachname'),
_('Titel nachgestellt'),
_('Benutzername'),
_('Adresse'),
_('Telefonnr.'),
_('E-Mail'),
_('Anmeldedatum'),
_('Matrikelnummer'),
_('Studiengänge'),
_('Position'),
];
foreach ($courses as $course) {
if ($GLOBALS['perm']->have_studip_perm('dozent', $course->id)) {
$members = $course->getMembersData();
$filename = FileManager::cleanFileName('Teilnehmendenexport ' . $course->Name . '.' . $export_format);
$filepath = $tmp_folder . '/'. $filename;
$this->render_spreadsheet($header, $members, $export_format, $filename, $filepath);
}
}
$archive_file_path = $GLOBALS['TMP_PATH'] . '/archiv.zip';
$archive_filename = 'Export_Teilnehmendendaten.zip';
FileArchiveManager::createArchiveFromPhysicalFolder($tmp_folder, $archive_file_path);
rmdirr($tmp_folder);
$this->render_temporary_file($archive_file_path, $archive_filename, 'application/zip');
}
/**
* Return a specifically action or all available actions
......@@ -1432,12 +1507,20 @@ class Admin_CoursesController extends AuthenticatedController
22 => [
'name' => _('Teilnehmendenexport'),
'title' => _('Teilnehmendenexport'),
'url' => 'dispatch.php/admin/user/batch_export_members',
'url' => 'dispatch.php/admin/courses/batch_export_members',
'dialogform' => true,
'multimode' => true,
'partial' => 'batch_export_members.php'
'partial' => 'export_members.php'
],
23 => [
'name' => _('Nachricht schreiben'),
'title' => _('Nachricht schreiben'),
'url' => 'dispatch.php/massmail/quick/courses',
'dialogform' => true,
'multimode' => true,
'partial' => 'massmail.php'
]
];
if (!$GLOBALS['perm']->have_perm('admin')) {
......@@ -1679,11 +1762,20 @@ class Admin_CoursesController extends AuthenticatedController
*/
private function getTeacherWidget($institut_id = null)
{
$institut_id = $institut_id ?: $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT;
if (str_contains($institut_id, '_')) {
$institut_id = substr($institut_id, 0, strpos($institut_id, '_'));
if ($institut_id) {
if (str_contains($institut_id, '_')) {
$institut_id = substr($institut_id, 0, strpos($institut_id, '_'));
$GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', 1);
} else {
$GLOBALS['user']->cfg->store('MY_INSTITUTES_INCLUDE_CHILDREN', 0);
}
} else {
$institut_id = $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT;
}
$teachers = [];
$include_children = $GLOBALS['user']->cfg->MY_INSTITUTES_INCLUDE_CHILDREN ? ' OR Institute.fakultaets_id = :institut_id ' : '';
if ($institut_id) {
$teachers = DBManager::get()->fetchAll("
SELECT auth_user_md5.*, user_info.*
......@@ -1691,8 +1783,8 @@ class Admin_CoursesController extends AuthenticatedController
LEFT JOIN user_info ON (auth_user_md5.user_id = user_info.user_id)
INNER JOIN user_inst ON (user_inst.user_id = auth_user_md5.user_id)
INNER JOIN Institute ON (Institute.Institut_id = user_inst.Institut_id)
WHERE (Institute.Institut_id = :institut_id OR Institute.fakultaets_id = :institut_id)
AND auth_user_md5.perms = 'dozent'
WHERE (Institute.Institut_id = :institut_id $include_children)
AND user_inst.inst_perms = 'dozent'
GROUP BY auth_user_md5.user_id
ORDER BY auth_user_md5.Nachname ASC, auth_user_md5.Vorname ASC
", [
......@@ -1743,6 +1835,7 @@ class Admin_CoursesController extends AuthenticatedController
$GLOBALS['user']->cfg->ADMIN_COURSES_SEARCHTEXT
);
$search->setOnSubmitHandler("STUDIP.AdminCourses.App.changeFilter({search: $(this).find('input').val()}); return false;");
$search->setOnClearHandler("STUDIP.AdminCourses.App.changeFilter({search: ''});");
$sidebar->addWidget($search, 'filter_search');
}
......
......@@ -156,7 +156,13 @@ class Admin_DatafieldsController extends AuthenticatedController
} elseif ($type === 'studycourse') {
$datafield->object_class = Request::option('object_class');
} else {
$datafield->object_class = array_sum(Request::getArray('object_class')) ?: null;
$object_class = Request::getArray('object_class');
if (empty($object_class) || (count($object_class) === 1 && $object_class[0] === 'NULL')) {
$object_class = null;
} else {
$object_class = array_sum($object_class);
}
$datafield->object_class = $object_class;
}
$datafield->edit_perms = Request::get('edit_perms');
$datafield->view_perms = Request::get('visibility_perms');
......@@ -188,6 +194,7 @@ class Admin_DatafieldsController extends AuthenticatedController
$this->institutes = Institute::getMyInstitutes();
if (!$this->object_typ) {
$this->render_action('type_select');
return;
}
if (Request::isXhr() && $this->type_name) {
......