Merge branch 'master' into feat/external-apis

This commit is contained in:
Sarah Vaupel 2022-05-27 01:50:24 +02:00
commit 25cb7f047a
197 changed files with 9605 additions and 16879 deletions

View File

@ -1,10 +1,17 @@
{
"presets": [
["@babel/preset-env", { "useBuiltIns": "usage" }]
["env", {
"useBuiltIns": "usage",
"targets": { "node": "current" }
}
]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }],
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-private-property-in-object", { "loose": true }],
["@babel/plugin-transform-modules-commonjs"],
["@babel/transform-runtime"]
]
}

View File

@ -11,9 +11,10 @@
"flatpickr": "readonly",
"$": "readonly"
},
"parser": "babel-eslint",
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 2018,
"requireConfigFile": false,
"ecmaFeatures": {
"legacyDecorators": true
}

View File

@ -51,7 +51,7 @@ npm install:
- install -v -T -m 0644 ${APT_SOURCES_LIST} /etc/apt/sources.list
- apt-get update -y
- npm install -g n
- n 14.8.0
- n 14.19.1
- export PATH="${N_PREFIX}/bin:$PATH"
- npm install -g npm
- hash -r

View File

@ -2,6 +2,132 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [26.0.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v26.0.0...v26.0.1) (2022-04-22)
### Bug Fixes
* **frontend:** various fe incompatabilities with updated tooling ([46530c6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/46530c6c644bec65c2219fd9b5830bc5bb8e9baf))
## [26.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.3...v26.0.0) (2022-04-22)
### ⚠ BREAKING CHANGES
* **system-message:** fix on-volatile-cluster-settings model default
### Bug Fixes
* **system-message:** fix on-volatile-cluster-settings model default ([4027f31](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4027f3144b613dbe67ca44f7a3142c13ff6f4dff))
* **webpack:** switch to wp-5 assets ([ec4d710](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec4d710ce0a5429160365397ad17c23b71c0a4cf))
## [25.29.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.2...v25.29.3) (2022-04-21)
### Bug Fixes
* **system-message:** add volatile cluster setting model default ([6655582](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6655582ace098808bfcea90ca85fce2fe0024d2b))
## [25.29.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.1...v25.29.2) (2022-04-21)
## [25.29.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.29.0...v25.29.1) (2022-04-21)
### Bug Fixes
* update lodash.debounce and defer imports ([f03dae6](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f03dae66e92a36ca7cea01fa8b508f7f8612200a))
## [25.29.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.28.0...v25.29.0) (2022-04-21)
### Features
* **workflows:** additional text field types ([4a34344](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4a34344c332291ceaed192ee1f0a73d97d855eeb))
### Bug Fixes
* **workflows:** always show navigation item ([82a4ecc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/82a4eccaa4fc5d6df3ff0bc71a025973f12c7dfd))
* **workflows:** properly distinguish anonymous/automatic ([21a1fb5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/21a1fb543b347a871b21db636d217e0816bd5388))
## [25.28.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.27.0...v25.28.0) (2022-04-20)
### Features
* **forms:** honeypots for unauthorized users ([8085c30](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8085c30420dcca9d8c493da8241a5e10d9ef8122))
* **frontend:** remove deprecated tail.DateTime ([6bfbff4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6bfbff41f6081dd65b0a4afa9155a125f31de973))
* **system-msg:** add volatile cluster settings to system message forms ([f8f9dc0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f8f9dc0621a18862bcf83364b874e176553ed7b1))
* **system-msg:** display system status messages on volatile cluster settings only ([da253f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da253f7fbdb919bb9cc52310148dcc28c382cfa4))
* **system-msg:** display volatile cluster settings in msg list ([32bed15](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/32bed159615b96ebab23766d748a844b1ba42f41))
* **translation mistakes:** courses and landing page ([9cff722](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9cff7220231a227859c771b720084e8a6952b5c1))
* **translation mistakes:** done ([229e379](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/229e3793c6266a2936ffb32854cb14e0bb0ba510))
* **translation mistakes:** megre request ([ff07768](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff077680b6fde39f4f82ba162781e471a9f3071c))
* **translation mistakes:** until admin ([43e5f9f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/43e5f9f7aa340d564557ec499859049dcb2ac194))
### Bug Fixes
* **system-msg:** use correct required features for form elems ([b99cda0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b99cda06aa22b3b9a3c026f80873cb2b68d93cdb))
## [25.27.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.26.0...v25.27.0) (2022-02-12)
### Features
* **communication:** add recipient option for course participants in at least one tutorial ([8dabb63](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8dabb63603341c7e2d7dadb95deeb77f864c14a0))
* **course-users:** export eppn to csv and json ([3c79703](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3c797039cc0784a2831167c11ed1b7bb8ff78daa))
* **course-users:** export eppn to csv and json ([6feefeb](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6feefeb3e16c8b2327cdb2746a8263394f9293f9))
* **courses:** add search bars for shorthands and titles ([8e1b9b9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8e1b9b9abae0c794ce12f10e5f75f410f903d927))
* **exam-users:** allow resolving exam users by eppn on csv-import ([6a041dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a041dc4c9c6ae62c7c7f1641eeb2f5417f32b8d))
* **exam-users:** allow resolving exam users by eppn on csv-import ([d677d35](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d677d35319332aae28410d1b7cdf09d295920fac))
* **exam-users:** export eppn for exam users ([ff1fe20](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ff1fe20efed340529a3a12858d82d668e5fe2e85))
* **exam-users:** export eppn for exam users ([d4ba513](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d4ba513d7948cc6e7136bd05543edb9d9764f78c))
* **submissions:** add option 'Set corrections as done' ([880eb3b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/880eb3b1ad2d5e4b12f767eb68d29eabfbd79e17))
* **submissions:** Apply suggestions to reduce lines of code ([2f1ecd3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2f1ecd397936f4e4ac018eb4f9aaf49da37e1e81))
### Bug Fixes
* **exams:** exam results of non-registered users now show correctly ([b294b1c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b294b1cfc4bab4b5ec5247d37097873748759727))
* **submissions:** add check if users in `groupMembers` are already submittors on submission ([4854d9c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4854d9c8661cc47447a794a4f2963e7e40fe678d))
* **submissions:** notDE, notEN for unambiguous negation ([ae66fdf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ae66fdfb8aa51cd7f6e6ad4e263e52cd7043abcb))
* **submissions:** shorter solution: remove check for `CourseParticipantActive` ([a358cdd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a358cdd100065014a1b7dc1055d7fc2ea1265011))
## [25.26.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.25.0...v25.26.0) (2022-02-04)
### Features
* **csv:** add export-exam-label as csv option ([de917a8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/de917a8d8646c4df599d945bc4979e226dd5192f))
* **eo-exams:** select column for exam list in case of actions ([42f58da](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/42f58da44fe1b0cc016979b9cde0300832cc0ae1))
* **eoexamsr:** implement label sorting ([808c2fc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/808c2fc7708cbf69172cc348e8a2c47c641287b1))
* **eoexamsr:** introduce GET param to control synced display ([09261ac](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/09261ac7578d8abdcaa39bbdcf12fc6ddad9ce22))
* **eoexamsr:** use user get-synced setting if no param present ([e60d125](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/e60d125e05bcf152b57fa132b4957405c40ae03f))
* **labels:** actions for setting and removing labels ([9e81f03](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9e81f03742586ef40e8ff896e303ba11d95f120a))
* **labels:** hide csv export option for non-exam offices ([ec55a40](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ec55a40bc11500105de0b9b9c5cbeb1ba035d53f))
* **labels:** label filter ([544b9ef](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/544b9ef76260eb9aecb1a5ee2ca3467f08cd99f7))
* **labels:** set export label on exam csv export ([4557fdd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4557fdda928fa5f24f4da48aff44fa8b8d955344))
* **labels:** set export label on external exam csv export ([2071f59](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2071f5912d4d183abb55f258450b374821f426e6))
* **profile:** upsert eo-labels on form submit ([cae652b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cae652b512137d3a85248d164fb16a6a7d8c097f))
* **user:** add get-labels to exam office user setting ([5a3c590](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5a3c590b72394fb1fd4544aace2a762300b4551d))
* **user:** add get-labels user setting ([6a10bd7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6a10bd78f55068eca71a0c190dcee5b066430b93))
* **user:** introduce exam office user settings ([6788f92](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6788f923ed10759a4d5235c119b94e4c2a35a8b4))
### Bug Fixes
* fix typo ([0dffa04](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0dffa04caa141e8e5c060ba342610ebd0049d548))
* **subs:** primarily order subs list by assigned time ([803d8b3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/803d8b30df9d8764dd515476f942c0e08e1da1f6))
* avoid column-off-by-one with URL-links to tr elems ([dbdd3dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/dbdd3dc5652ea274c91efc1c25f161c6fd3d850d))
* hlint ([2286086](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/22860863e59f0f927d6424552b913297fee24f14))
* **eo-exams:** display exams without label ([5fe01ce](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5fe01ce8dc87e4a318be1885a053b7403813cffa))
* **eo-exams:** fix eo-labels query ([eba56e4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eba56e4d621aa62fdee7535c8b83e0b61f0352d8))
* **labels:** correct forced bool value for no export label ([7b16351](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/7b16351e4b84eba0be1db7d723b4953e991b42b5))
* **labels:** fix exam-label delete action ([b1991ee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b1991eead90a21a296fa0436485ea2532223c72d))
* **labels:** implement label deletion on ProfileR ([da39b05](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/da39b05627058a1472929b46ae1c2adb0b1fe2c9))
* **tests:** complete test user definition ([11b7786](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/11b77867acd83a1fe23341d6aa9b0e3fd66506c0))
## [25.25.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.24.1...v25.25.0) (2022-01-21)

View File

@ -236,6 +236,8 @@ user-defaults:
download-files: false
warning-days: 1209600
show-sex: false
exam-office-get-synced: true
exam-office-get-labels: true
# During central allocations lecturer-given ratings of applications (as
# ExamGrades) are combined with a central priority.
@ -292,6 +294,7 @@ file-source-prewarm:
bot-mitigations:
- only-logged-in-table-sorting
- unauthorized-form-honeypots
volatile-cluster-settings-cache-time: 10

View File

@ -10,6 +10,8 @@
--color-success-dark: #1ca64c
--color-info: #c4c4c4
--color-info-dark: #919191
--color-nonactive: #efefef
--color-nonactive-dark: #9a989e
--color-lightblack: #1A2A36
--color-lightwhite: #fcfffa
--color-grey: #B1B5C0
@ -165,7 +167,7 @@ h4
margin-top: var(--current-header-height)
margin-left: 0
:target:not(table, .show-hide__toggle)::before
:target:not(table, tr, .show-hide__toggle)::before
content: ""
display: block
height: var(--current-header-height)
@ -740,6 +742,9 @@ section
.notification-success
color: var(--color-success-dark)
.notification-nonactive
color: var(--color-nonactive)
// "Heated" element.
// Set custom property "--hotness" to a value from 0 to 1 to turn
// the element's background to a color on a gradient from green to red.
@ -1485,6 +1490,9 @@ a.breadcrumbs__home
&--success
border-left-color: var(--color-success)
&--disabled
border-left-color: var(--color-nonactive)
.active-allocations__wrapper
@ -1746,3 +1754,28 @@ video
font-size: .5em
font-family: var(--font-monospace)
color: var(--color-fontsec)
.exam-office-label
--lbl-padding-vert: 5px
--lbl-padding-hori: 15px
padding: var(--lbl-padding-vert) var(--lbl-padding-hori)
border-radius: 20px 20px 20px 20px
font-weight: 600
text-align: center
width: fit-content
margin: 0 auto
&.success
background-color: var(--color-success-dark)
color: var(--color-lightwhite)
&.error
background-color: var(--color-error)
color: var(--color-lightwhite)
&.warning
background-color: var(--color-warning)
color: var(--color-lightwhite)
&.info
background-color: var(--color-lightblack)
color: var(--color-lightwhite)
&.nonactive
background-color: var(--color-nonactive)
color: var(--color-nonactive-dark)

View File

@ -43,7 +43,7 @@ export class EventManager {
}
removeAllEventListenersFromUtil() {
this._debugLog('removeAllEventListenersFromUtil',);
this._debugLog('removeAllEventListenersFromUtil');
for (let eventWrapper of this._registeredListeners) {
let element = eventWrapper.element;
element.removeEventListener(eventWrapper.eventType, eventWrapper.eventHandler);
@ -73,7 +73,7 @@ export class EventWrapper {
_eventType;
_eventHandler;
_element;
_options
_options;
constructor(_eventType, _eventHandler, _element, _options) {
if(!_eventType || !_eventHandler || !_element) {

View File

@ -1,4 +1,4 @@
import * as debounce from 'lodash.debounce';
import debounce from 'lodash.debounce';
export const MOVEMENT_INDICATOR_ELEMENT_CLASS = 'movement-indicator';
const MOVEMENT_DEBOUNCE = 250;

View File

@ -1,6 +1,6 @@
/* global global:writable */
import * as semver from 'semver';
import semver from 'semver';
import sodium from 'sodium-javascript';
import { HttpClient } from '../../services/http-client/http-client';
@ -469,7 +469,7 @@ export class StorageManager {
},
body: JSON.stringify(requestBody),
}).then(
(response) => response.json()
(response) => response.json(),
).then((response) => {
console.log('storage-manager got key from response:', response, 'with options:', options);
if (response.salt !== requestBody.salt || response.timestamp !== requestBody.timestamp) {

View File

@ -1,6 +1,6 @@
const DEBUG_MODE = /localhost/.test(window.location.href) ? 0 : 0;
import * as defer from 'lodash.defer';
import defer from 'lodash.defer';
class Overhang {
colSpan;

View File

@ -1,4 +1,4 @@
import * as toposort from 'toposort';
import toposort from 'toposort';
const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;

View File

@ -165,7 +165,7 @@ export class Alerts {
this._elevateAlerts();
}
}
};
_createAlertElement(type, content, icon = 'info-circle') {
const alertElement = document.createElement('div');

View File

@ -95,13 +95,13 @@ export class AsyncForm {
headers: headers,
body: body,
}).then(
(response) => response.json()
(response) => response.json(),
).then(
(response) => this._processResponse(response[0])
(response) => this._processResponse(response[0]),
).catch(() => {
const failureMessage = this._app.i18n.get('asyncFormFailure');
this._processResponse({ content: failureMessage });
this._element.classList.remove(ASYNC_FORM_LOADING_CLASS);
});
}
};
}

View File

@ -3,8 +3,8 @@ import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-mana
import { Datepicker } from '../form/datepicker';
import { HttpClient } from '../../services/http-client/http-client';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
import * as debounce from 'lodash.debounce';
import * as throttle from 'lodash.throttle';
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
import './async-table-filter.sass';
import './async-table.sass';
@ -383,7 +383,7 @@ export class AsyncTable {
url = window.location.origin + window.location.pathname + url;
}
this._updateTableFrom(url);
}
};
_getClickDestination(el) {
if (!el.matches('a') && !el.querySelector('a')) {
@ -407,7 +407,7 @@ export class AsyncTable {
}
this._updateTableFrom(url.href);
}
};
// fetches new sorted element from url with params and replaces contents of current element
_updateTableFrom(url, callback, isPopState) {
@ -458,7 +458,7 @@ export class AsyncTable {
callback(this._element);
this._windowStorage.remove('cssIdPrefix');
}
}).catch((err) => console.error(err)
}).catch((err) => console.error(err),
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
}

View File

@ -102,7 +102,7 @@ class CheckAllColumn {
_table;
_column;
_eventManager
_eventManager;
_checkAllCheckbox;
_checkboxId = 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
@ -146,7 +146,7 @@ class CheckAllColumn {
_updateCheckAllCheckboxState() {
const allChecked = this._column.every(cell =>
cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR).checked
cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR).checked,
);
this._checkAllCheckbox.checked = allChecked;
}

View File

@ -13,7 +13,7 @@ const COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron';
export class CourseTeaser {
_element;
_eventManager
_eventManager;
constructor(element) {
if (!element) {

View File

@ -133,7 +133,7 @@ export class ExamCorrect {
this._cIndices = new Map(
[...this._element.querySelectorAll('[uw-exam-correct-header]')]
.map((header) => [header.getAttribute('uw-exam-correct-header'), header.cellIndex])
.map((header) => [header.getAttribute('uw-exam-correct-header'), header.cellIndex]),
);
if (this._resultSelect && this._resultGradeSelect) {
@ -220,9 +220,9 @@ export class ExamCorrect {
headers: EXAM_CORRECT_HEADERS,
body: JSON.stringify(body),
}).then(
(response) => response.json()
(response) => response.json(),
).then(
(response) => this._processResponse(body, response, body.user)
(response) => this._processResponse(body, response, body.user),
).catch((error) => {
console.error('Error while validating user input', error);
});
@ -312,9 +312,9 @@ export class ExamCorrect {
headers: EXAM_CORRECT_HEADERS,
body: JSON.stringify(body),
}).then(
(response) => response.json()
(response) => response.json(),
).then(
(response) => this._processResponse(body, response, user, undefined, { results: results, result: result })
(response) => this._processResponse(body, response, user, undefined, { results: results, result: result }),
).catch((error) => {
console.error('Error while processing response', error);
});
@ -553,8 +553,8 @@ export class ExamCorrect {
headers: EXAM_CORRECT_HEADERS,
body: JSON.stringify(body),
}).then(
(response) => response.json()
).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result })
(response) => response.json(),
).then((response) => this._processResponse(body, response, userElem.getAttribute(EXAM_CORRECT_USER_ATTR), row, { results: results.partResults, result: results.result }),
).catch(console.error);
}

View File

@ -1,4 +1,4 @@
import * as debounce from 'lodash.debounce';
import debounce from 'lodash.debounce';
import { Utility } from '../../core/utility';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
@ -50,7 +50,7 @@ export class AutoSubmitInput {
this._element.classList.remove(AUTO_SUBMIT_INPUT_INITIALIZED_CLASS);
}
autoSubmit = () => {
autoSubmit() {
this._form.submit();
}
}

View File

@ -1,727 +0,0 @@
@charset "UTF-8";
/*
| tail.datetime - The vanilla way to select dates and times!
| @file ./less/tail.datetime-default-green.less
| @author SamBrishes <sam@pytes.net>
| @version 0.4.13 - Beta
|
| @website https://github.com/pytesNET/tail.DateTime
| @license X11 / MIT License
| @copyright Copyright © 2018 - 2019 SamBrishes, pytesNET <info@pytes.net>
*/
/* @start MAIN CALENDAR */
.tail-datetime-calendar, .tail-datetime-calendar *, .tail-datetime-calendar *:before,
.tail-datetime-calendar *:after{
box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.tail-datetime-calendar{
top: 0;
left: 0;
width: 275px;
height: auto;
margin: 15px;
padding: 0;
z-index: 15;
display: block;
position: absolute;
visibility: hidden;
direction: ltr;
border-collapse: separate;
/* font-family: "Open Sans", Calibri, Arial, sans-serif; */
background-color: white;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
}
.tail-datetime-calendar:after{
clear: both;
content: "";
display: block;
font-size: 0;
visibility: hidden;
}
.tail-datetime-calendar.calendar-static{
top: auto;
left: auto;
margin-left: auto;
margin-right: auto;
position: static;
visibility: visible;
}
.tail-datetime-calendar button.calendar-close{
top: 100%;
right: 15px;
color: #303438;
width: 35px;
height: 25px;
margin: 1px 0 0 0;
padding: 5px 10px;
opacity: 0.5;
outline: none;
display: inline-block;
position: absolute;
font-size: 14px;
line-height: 1.125em;
text-shadow: none;
background-color: white;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjMzAzNDM4IiBkP\
SJNNy40OCA4bDMuNzUgMy43NS0xLjQ4IDEuNDhMNiA5LjQ4bC0zLjc1IDMuNzUtMS40OC0xLjQ4TDQuNTIgOCAuNzcgNC4y\
NWwxLjQ4LTEuNDhMNiA2LjUybDMuNzUtMy43NSAxLjQ4IDEuNDhMNy40OCA4eiIvPjwvc3ZnPg==");
background-repeat: no-repeat;
background-position: center center;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 0 0 3px 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3125);
transition: opacity 142ms linear;
-webkit-transition: opacity 142ms linear;
}
.tail-datetime-calendar button.calendar-close:hover{
opacity: 1;
}
/* @end MAIN CALENDAR */
/* @start CALENDAR TOOLTIP */
.tail-datetime-calendar .calendar-tooltip{
color: white;
width: auto;
margin: 0;
padding: 0;
display: block;
position: absolute;
background-color: #202428;
border-radius: 3px;
}
.tail-datetime-calendar .calendar-tooltip:before{
top: -7px;
left: 50%;
width: 0;
height: 0;
margin: 0 0 0 -6px;
content: "";
display: block;
position: absolute;
border-width: 0 7px 7px 7px;
border-style: solid;
border-color: transparent transparent #202428 transparent;
}
.tail-datetime-calendar .calendar-tooltip .tooltip-inner{
width: auto;
margin: 0;
padding: 4px 7px;
display: block;
font-size: 12px;
line-height: 14px;
}
/* @end CALENDAR TOOLTIP */
/* @start CALENDAR ACTIONs */
.tail-datetime-calendar .calendar-actions{
color: white;
width: 100%;
height: 36px;
margin: 0;
padding: 0;
display: table;
overflow: hidden;
border-spacing: 0;
border-collapse: separate;
background-color: var(--color-primary);
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 3px 3px 0 0;
}
.tail-datetime-calendar .calendar-actions span{
margin: 0;
padding: 0;
display: table-cell;
position: relative;
text-align: center;
line-height: 36px;
text-shadow: -1px -1px 0 var(--color-dark);
background-repeat: no-repeat;
background-position: center center;
}
.tail-datetime-calendar .calendar-actions span[data-action]{
cursor: pointer;
}
.tail-datetime-calendar .calendar-actions span.action{
width: 36px;
font-size: 22px;
}
.tail-datetime-calendar .calendar-actions span.label{
width: auto;
}
.tail-datetime-calendar .calendar-actions span:first-child:before{
right: -1px;
}
.tail-datetime-calendar .calendar-actions span:last-child:before{
left: -1px;
}
.tail-datetime-calendar .calendar-actions span:first-child:hover:before,
.tail-datetime-calendar .calendar-actions span:last-child:hover:before{
display: none;
}
.tail-datetime-calendar .calendar-actions span[data-action]:hover{
background-color: var(--color-dark);
}
.tail-datetime-calendar .calendar-actions span.action-prev{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSI2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgNiAxNiI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iT\
TYgMkwwIDhsNiA2VjJ6Ii8+PC9zdmc+");
}
.tail-datetime-calendar .calendar-actions span.action-next{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSI2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgNiAxNiI+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iT\
TAgMTRsNi02LTYtNnYxMnoiLz48L3N2Zz4=");
}
.tail-datetime-calendar .calendar-actions span.action-submit{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjZmZmZmZmIiBkP\
SJNMTIgNWwtOCA4LTQtNCAxLjUtMS41TDQgMTBsNi41LTYuNUwxMiA1eiIvPjwvc3ZnPg==");
}
.tail-datetime-calendar .calendar-actions span.action-cancel{
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC\
9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDEyIDE2Ij48cGF0aCBmaWxsPSIjZmZmZmZmIiBkP\
SJNNy40OCA4bDMuNzUgMy43NS0xLjQ4IDEuNDhMNiA5LjQ4bC0zLjc1IDMuNzUtMS40OC0xLjQ4TDQuNTIgOCAuNzcgNC4y\
NWwxLjQ4LTEuNDhMNiA2LjUybDMuNzUtMy43NSAxLjQ4IDEuNDhMNy40OCA4eiIvPjwvc3ZnPg==");
}
/* @end CALENDAR ACTIONs */
/* @start CALENDAR DATEPICKER */
.tail-datetime-calendar .calendar-datepicker{
width: 100%;
margin: 0;
padding: 0;
display: block;
position: relative;
}
.tail-datetime-calendar .calendar-datepicker table{
width: 100%;
margin: 0;
padding: 0;
border-spacing: 0;
border-collapse: separate;
}
.tail-datetime-calendar .calendar-datepicker table tr th,
.tail-datetime-calendar .calendar-datepicker table tr td{
color: #303438;
height: 30px;
padding: 0;
position: relative;
font-size: 13px;
text-align: center;
font-weight: normal;
text-shadow: none;
line-height: 30px;
background-color: transparent;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 0px;
}
.tail-datetime-calendar .calendar-datepicker table tr th{
color: white;
background-color: var(--color-lightblack);
}
.tail-datetime-calendar .calendar-datepicker table tr td{
cursor: pointer;
}
.tail-datetime-calendar .calendar-datepicker table tr td span.inner{
margin: 0;
padding: 0;
display: inline-block;
}
.tail-datetime-calendar .calendar-datepicker table tr td.date-disabled{
cursor: not-allowed;
color: #909498;
background-color: #F0F0F0;
}
.tail-datetime-calendar .calendar-datepicker table tr td.date-disabled:after{
left: 3px;
bottom: 3px;
width: 35px;
height: 1px;
margin: 0;
padding: 0;
content: "";
display: inline-block;
position: absolute;
background-color: #bfbfbf;
transform-origin: 2px -5px;
transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
.tail-datetime-calendar .calendar-datepicker table tr td.date-previous,
.tail-datetime-calendar .calendar-datepicker table tr td.date-next{
color: #909498;
background-color: #F0F0F0;
}
.tail-datetime-calendar .calendar-datepicker table tr td.date-today:before,
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick{
top: 5px;
width: 5px;
height: 5px;
margin: 0;
padding: 0;
z-index: 20;
content: "";
display: inline-block;
position: absolute;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 50%;
}
.tail-datetime-calendar .calendar-datepicker table tr td.date-today:before{
left: 5px;
background-color: #E67D1E;
}
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick{
right: 5px;
background-color: #202428;
}
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick:before,
.tail-datetime-calendar .calendar-datepicker table tr td .tooltip-tick:after{
display: none;
}
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day{
width: 14.28571429%;
height: 35px;
}
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day span.inner{
width: 31px;
height: 31px;
line-height: 29px;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-radius: 50%;
}
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week:hover span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day:hover span.inner{
border-color: #cccccc;
}
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-disabled span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-disabled span.inner,
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-disabled:hover span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-disabled:hover span.inner{
border-color: transparent;
}
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-select span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-select span.inner,
.tail-datetime-calendar .calendar-datepicker table tr th.calendar-week.date-select:hover span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-day.date-select:hover span.inner{
color: var(--color-fontsec);
border-color: var(--color-fontsec);
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade{
width: 33.33333333%;
height: 40px;
transition: color 142ms linear;
-webkit-transition: color 142ms linear;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month.date-today:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year.date-today:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade.date-today:before{
left: 50%;
margin-left: -2.5px;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner{
width: auto;
height: 31px;
line-height: 29px;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:after{
width: 20px;
height: 20px;
content: "";
z-index: 15;
display: inline-block;
position: absolute;
border-width: 1px;
border-style: solid;
border-color: transparent;
transition: all 142ms linear;
-webkit-transition: all 142ms linear;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:before{
top: 0;
left: 0;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month:hover span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year:hover span.inner:before,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade:hover span.inner:before{
top: 6px;
left: 6px;
border-top-color: #cccccc;
border-left-color: #cccccc;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner:after{
right: 0;
bottom: 0;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-month:hover span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year:hover span.inner:after,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade:hover span.inner:after{
right: 6px;
bottom: 6px;
border-right-color: #cccccc;
border-bottom-color: #cccccc;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-year,
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade{
width: 25%;
}
.tail-datetime-calendar .calendar-datepicker table tr td.calendar-decade span.inner{
height: 54px;
padding: 7px 15px;
text-align: left;
line-height: 20px;
}
/* @end CALENDAR DATEPICKER */
/* @start CALENDAR TIMEPICKER */
.tail-datetime-calendar .calendar-timepicker{
width: 100%;
margin: 0;
padding: 0;
display: block;
text-align: center;
border-width: 1px 0 0 0;
border-style: solid;
border-color: #d9d9d9;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field{
width: 28%;
margin: 0;
padding: 15px 0 7px 0;
display: inline-block;
position: relative;
text-align: center;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field:first-of-type{
text-align: right;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field:last-of-type{
text-align: left;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]{
color: #303438;
width: 100%;
height: 29px;
margin: 0;
z-index: 4;
padding: 3px 20px 3px 5px;
outline: 0;
display: inline-block;
position: relative;
font-size: 12px;
text-align: center;
line-height: 23px;
appearance: textfield;
-moz-appearance: textfield;
-webkit-appearance: textfield;
background-color: #F0F0F0;
border-width: 0;
border-style: solid;
border-color: transparent;
border-radius: 3px;
box-shadow: none;
-webkit-box-shadow: none;
transition: color 142ms linear, border 142ms linear, background 142ms linear;
-webkit-transition: color 142ms linear, border 142ms linear, background 142ms linear;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:hover{
color: #303438;
background-color: #E0E0E0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:focus{
color: #303438;
background-color: #E0E0E0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input[type="text"]:disabled{
cursor: not-allowed;
color: #A0A4A8;
background-color: #F6F6F6;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step{
min-width: 0px;
width: 20px;
height: 15px;
right: 0;
margin: 0;
padding: 0;
z-index: 15;
display: inline-block;
position: absolute;
background-color: #F0F0F0;
box-shadow: none;
-webkit-box-shadow: none;
transition: border 142ms linear, background 142ms linear;
-webkit-transition: border 142ms linear, background 142ms linear;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step:before{
top: 4px;
left: 50%;
width: 0;
height: 0;
margin: 0 0 0 -4px;
padding: 0;
content: "";
display: inline-block;
position: absolute;
transition: border 142ms linear;
-webkit-transition: border 142ms linear;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up{
top: 15px;
border-width: 0 0 1px 1px;
border-style: solid;
border-color: white;
border-radius: 0 2px 0 0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up:hover{
background-color: #E0E0E0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-up:before{
border-width: 0 4px 5px 4px;
border-style: solid;
border-color: transparent transparent #303438 transparent;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down{
top: 29px;
border-width: 1px 0 0 1px;
border-style: solid;
border-color: white;
border-radius: 0 0 2px 0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down:hover{
background-color: #E0E0E0;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field button.picker-step.step-down:before{
border-width: 5px 4px 0 4px;
border-style: solid;
border-color: #303438 transparent transparent transparent;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up{
border-color: rgba(255, 255, 255, 0.8);
background-color: var(--color-primary);
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up:hover{
background-color: var(--color-dark);
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button.step-up:before{
border-bottom-color: white;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down{
border-color: rgba(255, 255, 255, 0.8);
background-color: var(--color-primary);
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down:hover{
background-color: var(--color-dark);
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:focus + button + button.step-down:before{
border-top-color: white;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up{
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.8);
background-color: #F6F6F6;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up:hover{
background-color: #F6F6F6;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button.step-up:before{
border-bottom-color: #A0A4A8;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down{
cursor: not-allowed;
border-color: rgba(255, 255, 255, 0.8);
background-color: #F6F6F6;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down:hover{
background-color: #F6F6F6;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field input:disabled + button + button.step-down:before{
border-top-color: #A0A4A8;
}
.tail-datetime-calendar .calendar-timepicker .timepicker-field label{
color: #303438;
margin: 0;
padding: 0;
display: block;
font-size: 12px;
text-align: center;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch{
cursor: pointer;
margin: 15px 0 -5px 0;
display: block;
text-align: center;
vertical-align: top;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:before,
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:after{
width: auto;
margin: 0;
padding: 0 5px;
font-size: 12px;
line-height: 16px;
vertical-align: top;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:before{
content: attr(data-am);
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch:after{
content: attr(data-pm);
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]{
display: none;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span{
display: inline-block;
position: relative;
vertical-align: top;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span:before{
width: 50px;
height: 16px;
content: "";
display: inline-block;
vertical-align: top;
border-width: 1px;
border-style: solid;
border-color: var(--color-primary);
border-radius: 14px;
transition: border 284ms linear;
-webkit-transition: border 284ms linear;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"] + span:after{
top: 3px;
left: 4px;
right: 30px;
width: auto;
height: 10px;
margin: 0;
padding: 0;
content: "";
display: inline-block;
position: absolute;
background-color: var(--color-primary);
border-radius: 15px;
vertical-align: top;
transition: left 284ms linear, right 284ms linear 284ms, background 284ms linear;
-webkit-transition: left 284ms linear, right 284ms linear 284ms, background 284ms linear;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]:checked + span:before{
border-color: #E67D1E;
}
.tail-datetime-calendar .calendar-timepicker label.timepicker-switch input[type="checkbox"]:checked + span:after{
left: 30px;
right: 4px;
background-color: #E67D1E;
transition: right 284ms linear, left 284ms linear 284ms, background 284ms linear;
-webkit-transition: right 284ms linear, left 284ms linear 284ms, background 284ms linear;
}
.tail-datetime-calendar .calendar-actions + .calendar-timepicker{
border-width: 0;
}
/* @end CALENDAR TIMEPICKER */
/* @start RTL */
.tail-datetime-calendar.rtl{
direction: rtl;
}
.tail-datetime-calendar.rtl .calendar-actions span.action-next,
.tail-datetime-calendar.rtl .calendar-actions span.action-prev{
transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.date-disabled:after{
right: 3px;
transform: rotate(45deg);
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.date-today:before{
right: 5px;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td .tooltip-tick{
left: 5px;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month.date-today:before,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year.date-today:before,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade.date-today:before{
right: 50%;
margin-right: -2.5px;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month:hover span.inner:before,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year:hover span.inner:before,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade:hover span.inner:before{
right: 6px;
border-right-color: #cccccc;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month span.inner:after,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year span.inner:after,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade span.inner:after{
left: 0;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-month:hover span.inner:after,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-year:hover span.inner:after,
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade:hover span.inner:after{
left: 6px;
border-left-color: #cccccc;
}
.tail-datetime-calendar.rtl .calendar-datepicker table tr td.calendar-decade span.inner{
text-align: right;
}
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:first-child{
text-align: left;
padding-left: 0;
padding-right: 25px;
}
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:last-child{
text-align: right;
padding-left: 25px;
padding-right: 0;
}
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:first-child input[type="text"]{
margin-left: -1px;
margin-right: 0;
border-radius: 0 3px 3px 0;
}
.tail-datetime-calendar.rtl .calendar-timepicker .timepicker-field:last-child input[type="text"]{
margin-left: 0;
margin-right: -1px;
border-radius: 3px 0 0 3px;
}
/* @end RTL */
/*# sourceMappingURL=tail.datetime-default-green.map */

View File

@ -1,17 +1,9 @@
import datetime from 'tail.datetime';
import './datepicker.css';
import { Utility } from '../../core/utility';
import moment from 'moment';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
import * as defer from 'lodash.defer';
import defer from 'lodash.defer';
const KEYCODE_ESCAPE = 27;
const Z_INDEX_MODAL = 9999;
// should be the same as ATTR_SUBMIT_LOCKED in async-table util
// TODO move to global config
const ATTR_DATEPICKER_OPEN = 'submit-locked';
// INTERNAL (Uni2work specific) formats for formatting dates and/or times
const FORM_DATE_FORMAT = {
@ -20,10 +12,6 @@ const FORM_DATE_FORMAT = {
'datetime-local': moment.HTML5_FMT.DATETIME_LOCAL_SECONDS,
};
// FANCY (tail.datetime specific) formats for displaying dates and/or times
const FORM_DATE_FORMAT_DATE_DT = 'dd.mm.YYYY';
const FORM_DATE_FORMAT_TIME_DT = 'HH:ii:ss';
// FANCY (moment specific) formats for displaying dates and/or times
const FORM_DATE_FORMAT_DATE_MOMENT = 'DD.MM.YYYY';
const FORM_DATE_FORMAT_TIME_MOMENT = 'HH:mm:ss';
@ -36,38 +24,7 @@ const FORM_DATE_FORMAT_MOMENT = {
const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
const DATEPICKER_OPEN_CLASS = 'calendar-open';
const DATEPICKER_CONFIG = {
'global': {
// set default time to 00:00:00
timeHours: 0,
timeMinutes: 0,
timeSeconds: 0,
weekStart: 1,
dateFormat: FORM_DATE_FORMAT_DATE_DT,
timeFormat: FORM_DATE_FORMAT_TIME_DT,
// prevent the instance from closing when selecting a date before selecting a time
stayOpen: true,
// hide the close button (we handle closing the datepicker manually by clicking outside)
closeButton: false,
// disable the decades view because nobody will ever need it (i.e. cap the switch to the more relevant year view)
viewDecades: false,
},
'datetime-local': {},
'date': {
// disable date picker
timeFormat: false,
},
'time': {
// disable time picker
dateFormat: false,
},
};
@Utility({
selector: DATEPICKER_UTIL_SELECTOR,
@ -77,7 +34,6 @@ export class Datepicker {
// singleton Map that maps a formID to a Map of Datepicker objects
static datepickerCollections;
datepickerInstance;
_element;
elementType;
initialValue;
@ -113,48 +69,10 @@ export class Datepicker {
// store initial value prior to changing type
this.initialValue = this._element.value || this._element.getAttribute('value');
// manually set the type attribute to text because datepicker handles displaying the date
this._element.setAttribute('type', 'text');
// get all relevant config options for this datepicker type
const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
const datepickerConfig = DATEPICKER_CONFIG[this.elementType];
// additional position config (optional data-datepicker-position attribute in html) that can specialize the global config
const datepickerPosition = this._element.dataset.datepickerPosition;
if (datepickerPosition) {
datepickerGlobalConfig.position = datepickerPosition;
}
if (!datepickerConfig || !FORM_DATE_FORMAT[this.elementType]) {
if (!FORM_DATE_FORMAT[this.elementType]) {
throw new Error('Datepicker utility called on unsupported element!');
}
// FIXME dirty hack below; fix tail.datetime instead
// get date object from internal format before datetime does nasty things with it
let parsedMomentDate = moment(this.initialValue, [ FORM_DATE_FORMAT[this.elementType], FORM_DATE_FORMAT_MOMENT[this.elementType] ], true);
if (parsedMomentDate && parsedMomentDate.isValid()) {
parsedMomentDate = parsedMomentDate.toDate();
} else {
parsedMomentDate = undefined;
}
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale });
// reset date to something sane
if (parsedMomentDate)
this.datepickerInstance.selectDate(parsedMomentDate);
// insert the datepicker element (dt) after the form
this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling);
// if the input element is in any open modal, increase the z-index of the datepicker
if (this._element.closest('.modal--open')) {
this.datepickerInstance.dt.style.zIndex = Z_INDEX_MODAL;
}
// register this datepicker instance with the formID of the given element in the datepicker collection
const formID = this._element.form.id;
const elemID = this._element.id;
@ -171,98 +89,17 @@ export class Datepicker {
}
start() {
const setDatepickerDate = () => {
// try to parse the current input element value with fancy and internal format string
const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]);
const parsedMomentDateInternal = moment(this._element.value, FORM_DATE_FORMAT[this.elementType]);
// only set the datepicker date if the input is either in valid fancy format or in valid internal format
if (parsedMomentDate.isValid()) {
this.datepickerInstance.selectDate(parsedMomentDate.toDate());
} else if (parsedMomentDateInternal.isValid()) {
this.datepickerInstance.selectDate(parsedMomentDateInternal.toDate());
}
// reregister change event to prevent event loop
};
// change the selected date in the tail.datetime instance if the value of the input element is changed
const changeSelectedDateEvent = new EventWrapper(EVENT_TYPE.CHANGE, setDatepickerDate.bind(this), this._element, { once: true });
this._eventManager.registerNewListener(changeSelectedDateEvent);
// create a mutation observer that observes the datepicker instance class and sets
// the datepicker-open DOM attribute of the input element if the datepicker has been opened
let callback = (mutations) => {
for (const mutation of mutations) {
if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) {
this._element.setAttribute(ATTR_DATEPICKER_OPEN, true);
break;
}
}
};
this._eventManager.registerNewMutationObserver(callback.bind(this), this.datepickerInstance.dt, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
});
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
const focusOutEvent = new EventWrapper(EVENT_TYPE.FOCUS_OUT,(event => {
const hasFocus = event.relatedTarget !== null;
const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget);
const focussedIsNotElement = event.relatedTarget !== this._element;
const focussedIsInDocument = window.document.contains(event.relatedTarget);
if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument)
this.closeDatepickerInstance();
}).bind(this), window );
this._eventManager.registerNewListener(focusOutEvent);
// close the instance on click on any element outside of the datepicker (except the input element itself)
const clickOutsideEvent = new EventWrapper(EVENT_TYPE.CLICK, (event => {
const targetIsOutside = !this.datepickerInstance.dt.contains(event.target)
&& event.target !== this.datepickerInstance.dt;
const targetIsInDocument = window.document.contains(event.target);
const targetIsNotElement = event.target !== this._element;
if (targetIsOutside && targetIsInDocument && targetIsNotElement)
this.closeDatepickerInstance();
}).bind(this), window);
this._eventManager.registerNewListener(clickOutsideEvent);
// close the instance on escape keydown events
const escapeCloseEvent = new EventWrapper(EVENT_TYPE.KEYDOWN, (event => {
if (event.keyCode === KEYCODE_ESCAPE) {
this.closeDatepickerInstance();
}
}).bind(this), this._element);
this._eventManager.registerNewListener(escapeCloseEvent);
// format the date value of the form input element of this datepicker before form submission
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form);
const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form);
this._eventManager.registerNewListener(submitEvent);
}
destroy() {
this.datepickerInstance.remove();
this._eventManager.cleanUp();
this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS);
}
// DATEPICKER INSTANCE CONTROL
/**
* Closes the datepicker instance, releasing the lock on the input element.
*/
closeDatepickerInstance() {
if (!this._element.datepicker-open) {
throw new Error('Cannot close already closed datepicker instance!');
}
this._element.setAttribute(ATTR_DATEPICKER_OPEN, false);
this.datepickerInstance.close();
}
// FORMAT METHODS
/**
@ -277,11 +114,13 @@ export class Datepicker {
_submitHandler() {
this._unloadIsDueToSubmit = true;
this._element.setAttribute('type', 'text');
this.formatElementValue(false);
defer(() => { // Restore state after event loop is settled
this._unloadIsDueToSubmit = false;
this.formatElementValue(true);
this._element.setAttribute('type', this.elementType);
});
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import * as defer from 'lodash.defer';
import defer from 'lodash.defer';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
const FORM_ERROR_REPORTER_INITIALIZED_CLASS = 'form-error-remover--initialized';

View File

@ -6,7 +6,7 @@ import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input';
import { InteractiveFieldset } from './interactive-fieldset';
import { Datepicker } from './datepicker';
import * as defer from 'lodash.defer';
import defer from 'lodash.defer';
/**
* Key generator from an arbitrary number of FormData objects.

View File

@ -158,12 +158,12 @@ export class HideColumns {
switchColumnDisplay(th, hidden) {
hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden;
this.cellColumns(th).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
Array.from(this.cellColumns(th)).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
}
updateColumnDisplay(columnIndex, hidden) {
// console.debug('updateColumnDisplay', { columnIndex, hidden });
this._element.rows.forEach(row => {
Array.from(this._element.rows).forEach(row => {
const cell = this.getCol(row, columnIndex);
if (cell) {
@ -235,7 +235,7 @@ export class HideColumns {
}
updateHiderIcon(hider, hidden) {
hider.getElementsByClassName('fas').forEach(hiderIcon => {
Array.from(hider.getElementsByClassName('fas')).forEach(hiderIcon => {
hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash');
hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye');
});

View File

@ -14,7 +14,7 @@ const CHECKBOX_SELECTOR = '[type="checkbox"]';
export class CheckRange {
_lastCheckedCell = null;
_element;
_tableIndices
_tableIndices;
_columns = new Array();
constructor(element) {

View File

@ -206,7 +206,7 @@ export class MassInput {
if (this._massInputFormSubmitHandler) {
return this._massInputFormSubmitHandler(event);
}
}
};
_getMassInputSubmitButtons() {
return Array.from(this._element.querySelectorAll('button[type="submit"][name][value], .' + MASS_INPUT_SUBMIT_BUTTON_CLASS));

View File

@ -29,7 +29,7 @@ const MODALS_WRAPPER_OPEN_CLASS = 'modals-wrapper--open';
})
export class Modal {
_eventManager
_eventManager;
_element;
_app;
@ -123,18 +123,18 @@ export class Modal {
_onTriggerClicked = (event) => {
event.preventDefault();
this._open();
}
};
_onCloseClicked = (event) => {
event.preventDefault();
this._close();
}
};
_onKeyUp = (event) => {
if (event.key === 'Escape') {
this._close();
}
}
};
_open() {
this._element.classList.add(MODAL_OPEN_CLASS);
@ -162,9 +162,9 @@ export class Modal {
url: url,
headers: MODAL_HEADERS,
}).then(
(response) => this._app.htmlHelpers.parseResponse(response)
(response) => this._app.htmlHelpers.parseResponse(response),
).then(
(response) => this._processResponse(response.element)
(response) => this._processResponse(response.element),
);
}

View File

@ -1,7 +1,7 @@
import { Utility } from '../../core/utility';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
import './navbar.sass';
import * as throttle from 'lodash.throttle';
import throttle from 'lodash.throttle';
export const HEADER_CONTAINER_UTIL_SELECTOR = '.navbar__list-item--container-selector .navbar__link-wrapper';
const HEADER_CONTAINER_INITIALIZED_CLASS = 'navbar-header-container--initialized';

View File

@ -1,7 +1,7 @@
import { Utility } from '../../core/utility';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';
import './pageactions.sass';
import * as throttle from 'lodash.throttle';
import throttle from 'lodash.throttle';
export const PAGEACTION_SECONDARY_UTIL_SELECTOR = '.pagenav__list-item';
const PAGEACTION_SECONDARY_INITIALIZED_CLASS = '.pagenav-list-item--initialized';

Binary file not shown.

View File

@ -40,11 +40,11 @@ StudySubTermsChildName: Child-Name
MailTestFormEmail: Email address
MailTestFormLanguages: Language settings
TestDownload: Download test
BearerTokenUsageWarning: Using this interface you are able to encode essentially arbitrary permissions inte bearer tokens. This allows you to freely hand permissions off arbitrarily and without relevant restrictions. Only use this interface if you have discussed the consequences of the specific token, that you want to issue, with an experienced developer!
BearerTokenUsageWarning: Using this interface you are able to encode essentially arbitrary permissions into bearer tokens. This allows you to freely hand permissions off arbitrarily and without relevant restrictions. Only use this interface if you have discussed the consequences of the specific token, that you want to issue, with an experienced developer!
BearerTokenAuthorityGroups: Authority (groups)
BearerTokenAuthorityGroupsTip: All primary users of the groups listed here need to have the requisite permissions to access a route in order for the created token to grant permission to do so as well.
BearerTokenAuthorityGroupMissing: Group is required
BearerTokenAuthorityUsers: Authority (users
BearerTokenAuthorityUsers: Authority (users)
BearerTokenAuthorityUsersTip: All users listed here need to have the requisite permissions to access a route in order for the created token to grant permission to do so as well. The user issuing the token using this interface also needs to have permission to access that route (they are automatically added to the list of authorities).
BearerTokenAuthorityUnknownUser email: Could not find any user with email #{email}
BearerTokenRoutes: Permitted routes

View File

@ -126,7 +126,20 @@ InvalidCredentialsADTooManyContextIds: Benutzereintrag trägt zu viele Sicherhei
InvalidCredentialsADAccountExpired: Benutzereintrag abgelaufen
InvalidCredentialsADPasswordMustChange: Passwort muss geändert werden
InvalidCredentialsADAccountLockedOut: Benutzereintrag wurde durch Eindringlingserkennung gesperrt
LoginTitle: Authentifizierung
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
FormFieldWorkflowDatasetTip: Mindestens ein gekennzeichnetes Feld pro Datensatz muss ausgefüllt werden
LoginTitle: Authentifizierung
FormHoneypotWebsite: Webseite (URL)
FormHoneypotWebsiteTip: Link zu Ihrer Webseite
FormHoneypotWebsitePlaceholder: URL
FormHoneypotEmail: E-Mail
FormHoneypotEmailTip: Ihre E-Mail Adresse
FormHoneypotEmailPlaceholder: E-Mail
FormHoneypotName: Name
FormHoneypotNameTip: Ihr Name oder Ihre E-Mail Adresse
FormHoneypotNamePlaceholder: Name
FormHoneypotComment: Kommentar
FormHoneypotCommentPlaceholder: Kommentar
FormHoneypotFilled: Bitte füllen Sie keines der verstecken Felder aus

View File

@ -127,6 +127,20 @@ InvalidCredentialsADTooManyContextIds: Account carries to many security identifi
InvalidCredentialsADAccountExpired: Account expired
InvalidCredentialsADPasswordMustChange: Password needs to be changed
InvalidCredentialsADAccountLockedOut: Account disabled by intruder detection
LoginTitle: Authentication
FormFieldRequiredTip: Required fields
FormFieldWorkflowDatasetTip: At least one of the marked fields must be filled
LoginTitle: Authentication
FormHoneypotWebsite: Website (URL)
FormHoneypotWebsiteTip: Link to your website
FormHoneypotWebsitePlaceholder !ident-ok: URL
FormHoneypotEmail: Email
FormHoneypotEmailTip: Your email address
FormHoneypotEmailPlaceholder: Email
FormHoneypotName !ident-ok: Name
FormHoneypotNameTip: Your name or your email address
FormHoneypotNamePlaceholder !ident-ok: Name
FormHoneypotComment: Comment
FormHoneypotCommentPlaceholder: Comment
FormHoneypotFilled: Please do not fill in any of the hidden fields

View File

@ -4,6 +4,8 @@ FilterTerm !ident-ok: Semester
FilterCourseSchoolShort: Institut
FilterRegistered: Angemeldet
FilterCourseSearch: Volltext-Suche
FilterCourseSearchShorthand: Kürzel-Suche
FilterCourseSearchTitle: Titel-Suche
FilterCourseRegistered: Registriert
FilterCourseRegisterOpen: Anmeldung möglich
FilterCourseAllocation: Zentralanmeldung
@ -279,4 +281,4 @@ LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lTyp
CourseExamRegistrationTime: Angemeldet seit
CourseParticipantStateIsActiveFilter: Ansicht
CourseApply: Zum Kurs bewerben
CourseAdministrator: Kursadministrator:in
CourseAdministrator: Kursadministrator:in

View File

@ -4,6 +4,8 @@ FilterTerm: Semester
FilterCourseSchoolShort: Department
FilterRegistered: Enrolled
FilterCourseSearch: Text search
FilterCourseSearchShorthand: Shorthand search
FilterCourseSearchTitle: Title search
FilterCourseRegistered: Registered
FilterCourseRegisterOpen: Enrolment is allowed
FilterCourseAllocation: Central allocation
@ -245,8 +247,8 @@ CourseLecInviteExplanation: You were invited to be a course administrator.
CourseUserHasPersonalisedSheetFilesFilter: Participant has personalised sheet files for
HeadingCourseMembers: Participants
CourseAssistant: Assistant
CourseParticipantStateIsInactive: Participant is CourseParticipantStateIsInactive
CourseParticipantStateIsActive: Participant is aktive
CourseParticipantStateIsInactive: Participant is inactive
CourseParticipantStateIsActive: Participant is active
CourseUserSendMail: Send message to participant
CourseUserRegisterTutorial: Register tutorial
CourseUserRegisterExam: Register exam
@ -278,4 +280,4 @@ LecturerInvitationAccepted lType csh: You were registered as #{lType} for #{csh}
CourseExamRegistrationTime: Registered since
CourseParticipantStateIsActiveFilter: View
CourseApply: Apply for course
CourseAdministrator: Course administrator
CourseAdministrator: Course administrator

View File

@ -55,3 +55,24 @@ ExamOfficeFieldSubscribed: Abboniert
UtilExamClosed: Noten gemeldet
ExamFinishedOffice: Noten bekannt gegeben
ExamOfficeFieldForced: Forcierte Einsicht
ExamOfficeGetSynced: Synchronisiert-Status in Prüfungsliste anzeigen
ExamOfficeGetSyncedTip: Soll unter „Prüfungen“ der Synchronisiert-Status zu jeder Prüfung angezeigt werden? (Ein Deaktivieren dieser Option kann zu kürzeren Ladezeiten der Prüfungsliste führen.)
ExamLabel: Prüfungs-Label
ExamOfficeGetLabels: Labels in Prüfungsliste anzeigen
ExamOfficeGetLabelsTip: Sollen unter „Prüfungen“ die gesetzten Labels zu jeder Prüfung angezeigt werden?
ExamOfficeLabels: Prüfungs-Labels
ExamOfficeLabelsTip: Sie können hier Labels anlegen und verwalten, welche sie einzelnen Prüfungen über die Prüfungsliste (siehe „Prüfungen“) zuweisen können.
ExamOfficeLabelName !ident-ok: Name
ExamOfficeLabelStatus !ident-ok: Status
ExamOfficeLabelPriority: Priorität
ExamOfficeLabelAlreadyExists: Es existiert bereits ein Prüfungs-Label mit diesem Namen!
ExamOfficeExamsNoLabel: Kein Label
ExamSetLabel: Label setzen
ExamLabelsSet n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} gesetzt
ExamRemoveLabel: Label entfernen
ExamLabelsRemoved n@Int: #{n} Prüfungs-#{pluralDE n "Label" "Labels"} entfernt
ExamOfficeLabelSetLabelOnExport: Prüfungs-Label beim Export setzen
ExamOfficeLabelSetLabelOnExportTip t@Text: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Ihr aktuell für den CSV-Export eingestelltes Prüfungs-Label ist „#{t}“. Sie können das zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen ändern.
ExamOfficeLabelSetLabelOnExportForcedTip: Soll beim CSV-Export automatisch das Export-Label für die jeweilige Prüfung gesetzt werden? Von Ihnen gesetzte Prüfungs-Label sind ausschließlich für Sie sichtbar und können von jedem Prüfungsbeauftragten unabhängig voneinander verwaltet bzw. verwendet werden. Sie haben aktuell kein Export-Label festgelegt und können diese Option daher nicht auswählen. Sie können das beim CSV-Export zu setzende Prüfungs-Label unter „Export-Optionen“ oder in Ihren persönlichen Benutzereinstellungen wählen.

View File

@ -53,3 +53,24 @@ ExamOfficeFieldSubscribed: subscribed
UtilExamClosed: Exam achievements registered
ExamFinishedOffice: Exam achievements published
ExamOfficeFieldForced: Forced access
ExamOfficeGetSynced: Show synchronised status in exam list
ExamOfficeGetSyncedTip: Should the synchronised status be displayed in “Exams”? (Disabling this option may lead to shorter loading times of the exam list.)
ExamLabel: Exam label
ExamOfficeGetLabels: Show labels in exam list
ExamOfficeGetLabelsTip: Should the labels of each exam be displayed in “Exams”?
ExamOfficeLabels: Exam labels
ExamOfficeLabelsTip: Here you can add and manage labels, which you can assign exam list entries (see “Exams”).
ExamOfficeLabelName: Name
ExamOfficeLabelStatus: Status
ExamOfficeLabelPriority: Priority
ExamOfficeLabelAlreadyExists: There already exists an exam label with this name!
ExamOfficeExamsNoLabel: No label
ExamSetLabel: Set label
ExamLabelsSet n: Successfully set #{n} exam #{pluralEN n "label" "labels"}
ExamRemoveLabel: Remove label
ExamLabelsRemoved n: Successfully removed #{n} exam #{pluralEN n "label" "labels"}
ExamOfficeLabelSetLabelOnExport: Set exam label while exporting
ExamOfficeLabelSetLabelOnExportTip t: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. Your saved exam label for CSV export is currently “#{t}”. You can change the exam label set while exporting under “Export options” or in your user settings.
ExamOfficeLabelSetLabelOnExportForcedTip: Should the export label be set for the respective exam? Your set exam labels are exclusively visible to you and may be managed and used by each exam office member independently. You do not currently have any exam label selected as export label and therefor cannot active this setting. To set an exam label as export label, go to “Export options” or your user settings.

View File

@ -159,6 +159,8 @@ SubmissionDownloadMatriculations: Mit Matrikelnummern
SubmissionDownloadGroups: Mit festen Abgabegruppen
CorrAutoSetCorrector: Korrekturen verteilen
CorrDelete: Abgaben löschen
CorrSetCorrectionsDone: Korrekturen als abgeschlossen markieren
SetCorrectionsDone b@Bool: Korrekturen als #{notDE b} abgeschlossen markiert
SubmissionCorrected: Korrigiert
CorrectionSheets: Übersicht Korrekturen nach Blättern
CorrectionCorrectors: Übersicht Korrekturen nach Korrektor:innen
@ -262,4 +264,4 @@ CorrectionTableCsvSheetNameCourseCorrections tid@TermId ssh@SchoolId csh@CourseS
CorrectionTableCsvNameCorrections: abgaben
CorrectionTableCsvSheetNameCorrections: Abgaben
CorrectionTableCsvNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName}-abgaben
CorrectionTableCsvSheetNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Abgaben
CorrectionTableCsvSheetNameCourseUserCorrections tid@TermId ssh@SchoolId csh@CourseShorthand displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase displayName} Abgaben

View File

@ -159,6 +159,8 @@ SubmissionDownloadMatriculations: With matriculation numbers
SubmissionDownloadGroups: With registered submission groups
CorrAutoSetCorrector: Distribute corrections
CorrDelete: Delete submissions
CorrSetCorrectionsDone: Set corrections as done
SetCorrectionsDone b: Set corrections as #{notEN b} done
SubmissionCorrected: Marked
CorrectionSheets: Corrections by sheet
CorrectionCorrectors: Corrections by corrector

View File

@ -1,4 +1,4 @@
HelpRequestHeading: Support request / Suggestion
HelpRequestHeading: Support request/Suggestion
HelpIntroduction: If you have trouble using this website or if you find something that could be improved, please contact us even if you were already able to solve your problem by yourself! We are continually making changes and try to keep the site as intuitive as possible even for new users.
HelpProblemPage: Problematic page
@ -7,7 +7,7 @@ HelpUser: My user account
HelpEmail: Email
HelpAnonymous: Send no answers (anonymous)
HelpSubject: Subject
HelpRequest: Support request / Suggestion
HelpRequest: Support request/Suggestion
HelpSent: Your support request has been sent.
AdditionalFaqs: More frequently asked questions
HelpName: Name

View File

@ -21,9 +21,9 @@ AuthTagExamCorrector: User is exam corrector
AuthTagTutor: User is tutor
AuthTagTutorControl: Tutors have control over their tutorial
AuthTagTime: Time restrictions are fulfilled
AuthTagStaffTime: Time restrictions wrt. staff are fulfilled
AuthTagStaffTime: Time restrictions for teaching staff are fulfilled
AuthTagAllocationTime: Time restrictions due to a central allocation are fulfilled
AuthTagCourseTime: Time restrictions wrt. course visibility are fulfilled
AuthTagCourseTime: Time restrictions for course visibility are fulfilled
AuthTagCourseRegistered: User is enrolled in course
AuthTagAllocationRegistered: User participates in central allocation
AuthTagTutorialRegistered: User is tutorial participant

View File

@ -3,6 +3,8 @@ CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import
CsvFormatOptions: Dateiformat
CsvTimestamp: Zeitstempel
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
CsvExportLabel: Prüfungs-Label bei Export
CsvExportLabelTip: Soll beim CSV-Export von Prüfungsleistungen automatisch ein gegebenes Label für diese Prüfung gesetzt werden?
CsvPresetRFC: Standard-Konform (RFC 4180)
CsvPresetExcel: Excel-Kompatibel
CsvCustom: Benutzerdefiniert

View File

@ -3,6 +3,8 @@ CsvOptionsTip: These settings primarily affect CSV export. During import most se
CsvFormatOptions: File format
CsvTimestamp: Timestamp
CsvTimestampTip: Should the name of every exported csv file contain a timestamp?
CsvExportLabel: Exam label on export
CsvExportLabelTip: Should a given label be automatically set for an exam of which results are exported to CSV?
CsvPresetRFC: Standards-compliant (RFC 4180)
CsvPresetExcel: Excel compatible
CsvCustom: User defined

View File

@ -112,3 +112,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
AllocNotifyNewCourseForceOff: Nein
AllocNotifyNewCourseForceOn: Ja
Settings: Individuelle Benutzereinstellungen
FormExamOffice: Prüfungsverwaltung

View File

@ -112,4 +112,6 @@ LanguageChanged: Language changed successfully
AllocNotifyNewCourseDefault: System-wide setting
AllocNotifyNewCourseForceOff: No
AllocNotifyNewCourseForceOn: Yes
Settings: Settings
Settings: Settings
FormExamOffice: Exam Office

View File

@ -8,6 +8,7 @@ SystemMessageLastChanged: Zuletzt geändert
SystemMessageLastUnhide: Zuletzt un-versteckt
SystemMessageFrom: Sichtbar ab
SystemMessageTo: Sichtbar bis
SystemMessageOnVolatileClusterSettings: Sichtbar bei VolatileCluster-Einstellungen
SystemMessageAuthenticatedOnly: Nur angemeldet
SystemMessageSeverity: Schwere
SystemMessagePriority: Priorität
@ -38,3 +39,7 @@ SystemMessageEditTranslationSuccess: Übersetzung angepasst.
SystemMessageDeleteTranslationSuccess: Übersetzung entfernt.
RFC1766: RFC1766-Sprachcode
SystemMessageOnVolatileClusterSettingKey: VolatileCluster-Einstellung
SystemMessageOnVolatileClusterSettingValue: Wert
SystemMessageOnVolatileClusterSettingKeyExists: Für diese Einstellung existiert bereits ein Wert!

View File

@ -8,6 +8,7 @@ SystemMessageLastChanged: Last changed
SystemMessageLastUnhide: Last unhidden
SystemMessageFrom: Visible from
SystemMessageTo: Visible to
SystemMessageOnVolatileClusterSettings: Visible on VolatileCluster settings
SystemMessageAuthenticatedOnly: Only logged in users
SystemMessageSeverity: Severity
SystemMessagePriority: Priority
@ -38,3 +39,7 @@ SystemMessageEditTranslationSuccess: Successfully edited translation.
SystemMessageDeleteTranslationSuccess: Successfully deleted translation.
RFC1766: RFC1766 language code
SystemMessageOnVolatileClusterSettingKey: VolatileCluster setting
SystemMessageOnVolatileClusterSettingValue: Value
SystemMessageOnVolatileClusterSettingKeyExists: There already exists a value for this setting!

View File

@ -8,3 +8,6 @@ FieldSecondary: Nebenfach
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Adressen möglich
WeekDay: Wochentag
LdapIdentificationOrEmail: Campus-Kennung / E-Mail-Adresse
ClusterVolatileWorkflowsEnabled: Workflows aktiv
ClusterVolatileQuickActionsEnabled: Schnellzugriffsmenü aktiv

View File

@ -7,4 +7,7 @@ FieldPrimary: Major
FieldSecondary: Minor
MultiEmailFieldTip: Multiple emails addresses may be specified (comma-separated)
WeekDay: Day of the week
LdapIdentificationOrEmail: Campus account/email address
LdapIdentificationOrEmail: Campus account/email address
ClusterVolatileWorkflowsEnabled: Workflows enabled
ClusterVolatileQuickActionsEnabled: Quick actions enabled

View File

@ -50,7 +50,7 @@ BtnExamAutoOccurrenceNudgeUp: +
BtnExamAutoOccurrenceNudgeDown: -
BtnSetDisplayEmail: Set email address
BtnAuthLDAP: Change to campus account
BtnAuthPWHash: Change to Uni2work accont
BtnAuthPWHash: Change to Uni2work account
BtnPasswordReset: Reset password
BtnCsvExport: Export CSV file
BtnCsvImport: Import CSV file

View File

@ -10,7 +10,7 @@ LegalHeading: Legal
VersionHeading: Version history
SystemMessageHeading: Uni2work system message
SystemMessageListHeading: Uni2work system message
HeadingHelpRequest: Support request / Suggestion
HeadingHelpRequest: Support request/Suggestion
ProfileHeading: Settings
ProfileDataHeading: Personal information
CorrectorsChange: Adjust correctors

View File

@ -22,6 +22,7 @@ TableExamName !ident-ok: Name
TableExamTime: Termin
TableExamRegistration: Prüfungsanmeldung
TableExamResult: Prüfungsergebnis
TableExamLabel !ident-ok: Label
TableSheet: Blatt
TableLastEdit: Letzte Änderung
TableSubmission: Abgabenummer
@ -61,4 +62,7 @@ SelectColumn: Auswahl
CsvExport: CSV-Export
TableProportion c@Text of'@Text prop@Rational !ident-ok: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c@Text of'@Text !ident-ok: #{c}/#{of'}
TableExamFinished: Ergebnisse sichtbar ab
TableExamFinished: Ergebnisse sichtbar ab
TableExamOfficeLabel: Label-Name
TableExamOfficeLabelStatus: Label-Farbe
TableExamOfficeLabelPriority: Label-Priorität

View File

@ -22,6 +22,7 @@ TableExamName: Name
TableExamTime: Time
TableExamRegistration: Exam registration
TableExamResult: Exam result
TableExamLabel: Label
TableSheet: Sheet
TableLastEdit: Latest edit
TableSubmission: Submission-number
@ -61,4 +62,7 @@ SelectColumn: Selection
CsvExport: CSV export
TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c of': #{c}/#{of'}
TableExamFinished: Results visible from
TableExamFinished: Results visible from
TableExamOfficeLabel: Label name
TableExamOfficeLabelStatus: Label colour
TableExamOfficeLabelPriority: Label priority

View File

@ -4,6 +4,7 @@ RGCourseParticipants: Kursteilnehmer:innen
RGCourseLecturers: Kursverwalter:innen
RGCourseCorrectors: Korrektor:innen
RGCourseTutors: Tutor:innen
RGCourseParticipantsInTutorial: Kursteilnehmer:innen, die in mindestens einem Tutorium angemeldet sind
RGCourseUnacceptedApplicants: Nicht akzeptierte Bewerber:innen
RecipientToggleAll: Alle/Keine
CommCourseTestSubject customSubject@Text !ident-ok: [TEST] #{customSubject}
@ -136,6 +137,7 @@ MessageError: Fehler
MessageWarning: Warnung
MessageInfo !ident-ok: Information
MessageSuccess: Erfolg
MessageNonactive: Inaktiv
ShortFieldPrimary: HF
ShortFieldSecondary: NF
SheetGradingPassPoints': Bestehen nach Punkten

View File

@ -4,6 +4,7 @@ RGCourseParticipants: Course participants
RGCourseLecturers: Course administrators
RGCourseCorrectors: Course correctors
RGCourseTutors: Course tutors
RGCourseParticipantsInTutorial: Course participants who are registered for at least one tutorial
RGCourseUnacceptedApplicants: Applicants not accepted
RecipientToggleAll: All/None
CommCourseTestSubject customSubject: [TEST] #{customSubject}
@ -136,6 +137,7 @@ MessageError: Error
MessageWarning: Warning
MessageInfo: Information
MessageSuccess: Success
MessageNonactive: Inactive
ShortFieldPrimary: Mj
ShortFieldSecondary: Mn
SheetGradingPassPoints': Passing by points

View File

@ -0,0 +1,18 @@
ExamOfficeLabel
user UserId
name ExamOfficeLabelName
status MessageStatus
priority Int -- determines label ordering
UniqueExamOfficeLabel user name
deriving Generic
ExamOfficeExamLabel
exam ExamId
label ExamOfficeLabelId
UniqueExamOfficeExamLabel exam
deriving Generic
ExamOfficeExternalExamLabel
externalExam ExternalExamId
label ExamOfficeLabelId
UniqueExamOfficeExternalExamLabel externalExam
deriving Generic

View File

@ -1,8 +1,9 @@
-- Messages shown to all users as soon as they visit the site/log in (i.e.: "System is going down for maintenance next sunday")
-- Only administrators (of any school) should be able to create these via a web-interface
SystemMessage
SystemMessage json
from UTCTime Maybe -- Message is not shown before this date has passed (never shown, if null)
to UTCTime Maybe -- Message is shown until this date has passed (shown forever, if null)
onVolatileClusterSettings SystemMessageVolatileClusterSettings default="'[]'::jsonb" -- Message is shown when given volatile cluster settings have given values
newsOnly Bool default=false
authenticatedOnly Bool -- Show message to all users upon visiting the site or only upon login?
severity MessageStatus -- Success, Warning, Error, Info, ...

View File

@ -17,7 +17,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
lastAuthentication UTCTime Maybe -- last login date
created UTCTime default=now()
lastLdapSynchronisation UTCTime Maybe
ldapPrimaryKey Text Maybe
ldapPrimaryKey UserEduPersonPrincipalName Maybe
tokensIssuedAfter UTCTime Maybe -- do not accept bearer tokens issued before this time (accept all tokens if null)
matrikelnummer UserMatriculation Maybe -- optional immatriculation-string; usually a number, but not always (e.g. lecturers, pupils, guests,...)
firstName Text -- For export in tables, pre-split firstName from displayName
@ -35,6 +35,8 @@ User json -- Each Uni2work user has a corresponding row in this table; create
csvOptions CsvOptions "default='{}'::jsonb"
sex Sex Maybe
showSex Bool default=false
examOfficeGetSynced Bool default=true -- whether synced status should be displayed for exam results by default
examOfficeGetLabels Bool default=true -- whether labels should be displayed for exam results by default
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
@ -53,8 +55,8 @@ UserSystemFunction
UniqueUserSystemFunction user function
deriving Generic
UserExamOffice
user UserId
field StudyTermsId
user UserId
field StudyTermsId
UniqueUserExamOffice user field
deriving Generic
UserSchool -- Managed by users themselves, encodes "schools of interest"

21591
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "25.25.0",
"version": "26.0.1",
"description": "",
"keywords": [],
"author": "",
@ -10,7 +10,6 @@
"test": "run-s frontend:test yesod:test i18n:test",
"lint": "run-s frontend:lint yesod:lint",
"build": "run-s frontend:build yesod:build",
"cbt": "./cbt.sh",
"yesod:db": "./db.sh",
"yesod:start": "./start.sh",
"yesod:lint": "./hlint.sh",
@ -45,85 +44,86 @@
"defaults"
],
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.4",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-transform-runtime": "^7.11.0",
"@babel/preset-env": "^7.11.0",
"@commitlint/cli": "^10.0.0",
"@commitlint/config-conventional": "^10.0.0",
"@fortawesome/fontawesome-pro": "^5.14.0",
"autoprefixer": "^9.8.6",
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.9",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-proposal-decorators": "^7.17.9",
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
"@babel/plugin-transform-modules-commonjs": "^7.17.9",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@fortawesome/fontawesome-pro": "^6.1.1",
"autoprefixer": "^10.4.4",
"babel-core": "^6.26.3",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-loader": "^8.2.5",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"cbt_tunnels": "^1.2.2",
"changelog-parser": "^2.8.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"file-loader": "^5.1.0",
"fs-extra": "^8.1.0",
"glob": "^7.1.6",
"html-webpack-plugin": "^3.2.0",
"husky": "^2.7.0",
"jasmine-core": "^3.6.0",
"js-yaml": "^3.14.0",
"karma": "^5.1.1",
"karma-chrome-launcher": "^2.2.0",
"changelog-parser": "^2.8.1",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.7.1",
"eslint": "^8.13.0",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"jasmine-core": "^4.1.0",
"js-yaml": "^4.1.0",
"karma": "^6.3.19",
"karma-chrome-launcher": "^3.1.1",
"karma-cli": "^2.0.0",
"karma-jasmine": "^2.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"karma-jasmine": "^5.0.0",
"karma-jasmine-html-reporter": "^1.7.0",
"karma-mocha-reporter": "^2.2.5",
"karma-webpack": "^3.0.5",
"lint-staged": "^8.2.1",
"karma-webpack": "^5.0.0",
"lint-staged": "^12.4.0",
"lodash.debounce": "^4.0.8",
"mini-css-extract-plugin": "^0.8.2",
"mini-css-extract-plugin": "^2.6.0",
"npm-run-all": "^4.1.5",
"null-loader": "^2.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"null-loader": "^4.0.1",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.4.3",
"real-favicon-webpack-plugin": "^0.2.3",
"remove-files-webpack-plugin": "^1.4.3",
"remove-files-webpack-plugin": "^1.5.0",
"request": "^2.88.2",
"request-promise": "^4.2.6",
"resolve-url-loader": "^3.1.1",
"sass": "^1.26.10",
"sass-loader": "^7.3.1",
"semver": "^6.3.0",
"standard-version": "^9.1.0",
"standard-version-updater-yaml": "^1.0.2",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^2.3.8",
"tmp": "^0.1.0",
"typeface-roboto": "0.0.75",
"typeface-source-code-pro": "^1.1.3",
"typeface-source-sans-pro": "0.0.75",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-manifest-plugin": "^2.2.0",
"webpack-plugin-hash-output": "^3.2.1"
"resolve-url-loader": "^5.0.0",
"sass": "^1.50.1",
"sass-loader": "^12.6.0",
"semver": "^7.3.7",
"standard-version": "^9.3.2",
"standard-version-updater-yaml": "^1.0.3",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.1",
"tmp": "^0.2.1",
"typeface-roboto": "1.1.13",
"typeface-source-code-pro": "^1.1.13",
"typeface-source-sans-pro": "1.1.13",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-manifest-plugin": "^5.0.0"
},
"dependencies": {
"@babel/runtime": "^7.11.2",
"@juggle/resize-observer": "^2.5.0",
"core-js": "^3.6.5",
"@babel/runtime": "^7.17.9",
"@juggle/resize-observer": "^3.3.1",
"core-js": "^3.22.2",
"css.escape": "^1.5.1",
"js-cookie": "^2.2.1",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.defer": "^4.1.0",
"lodash.throttle": "^4.1.1",
"moment": "^2.27.0",
"npm": "^6.14.8",
"sodium-javascript": "^0.5.6",
"tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#master",
"moment": "^2.29.3",
"npm": "^8.7.0",
"npm-check-updates": "^12.5.9",
"sodium-javascript": "^0.8.0",
"toposort": "^2.0.2",
"whatwg-fetch": "^3.4.0"
"whatwg-fetch": "^3.6.2"
}
}

View File

@ -1,5 +1,5 @@
name: uniworx
version: 25.25.0
version: 26.0.1
dependencies:
- base
- yesod

File diff suppressed because it is too large Load Diff

2
routes
View File

@ -114,7 +114,7 @@
/user/storage-key StorageKeyR POST !free
/exam-office ExamOfficeR !exam-office:
/ EOExamsR GET !system-exam-office
/ EOExamsR GET POST !system-exam-office
/fields EOFieldsR GET POST
/users EOUsersR GET POST !system-exam-office
/users/invite EOUsersInviteR GET POST !system-exam-office

View File

@ -28,7 +28,7 @@ dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
) => WForm m (FormResult (CI Text))
dummyForm = do
mr <- getMessageRender
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing
wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & addAttr "autocomplete" "username" & addName PostLoginDummy) Nothing
where
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
@ -40,7 +40,7 @@ dummyLogin :: forall site.
( YesodAuth site
, YesodPersist site
, SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site AFormMessage
, RenderAFormSite site
, RenderMessage site DummyMessage
, RenderMessage site (ValueRequired site)
, Button site ButtonSubmit
@ -52,7 +52,7 @@ dummyLogin = AuthPlugin{..}
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard dummyForm
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin dummyForm
tp <- getRouteToParent
case loginRes of
FormFailure errs -> do
@ -69,7 +69,7 @@ dummyLogin = AuthPlugin{..}
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard dummyForm
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin dummyForm
let loginForm = wrapForm login FormSettings
{ formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR apName []

View File

@ -174,8 +174,8 @@ campusForm :: ( RenderMessage (HandlerSite m) FormMessage
campusForm = do
MsgRenderer mr <- getMsgRenderer
aFormToWForm $ CampusLogin
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing
<$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "" & addAttr "autocomplete" "username") Nothing
<*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
apLdap :: Text
apLdap = "LDAP"
@ -183,7 +183,7 @@ apLdap = "LDAP"
campusLogin :: forall site.
( YesodAuth site
, RenderMessage site CampusMessage
, RenderMessage site AFormMessage
, RenderAFormSite site
, RenderMessage site (ValueRequired site)
, RenderMessage site ADInvalidCredentials
, Button site ButtonSubmit
@ -195,7 +195,7 @@ campusLogin pool mode = AuthPlugin{..}
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin campusForm
tp <- getRouteToParent
resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
@ -250,7 +250,7 @@ campusLogin pool mode = AuthPlugin{..}
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard campusForm
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin campusForm
let loginForm = wrapForm login FormSettings
{ formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR apName []

View File

@ -37,8 +37,8 @@ hashForm :: ( RenderMessage (HandlerSite m) FormMessage
hashForm = do
MsgRenderer mr <- getMsgRenderer
aFormToWForm $ HashLogin
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder) & addAttr "autocomplete" "username") Nothing
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder) & addAttr "autocomplete" "current-password") Nothing
apHash :: Text
apHash = "PWHash"
@ -49,7 +49,7 @@ hashLogin :: forall site.
, SqlBackendCanRead (YesodPersistBackend site)
, PersistRecordBackend User (YesodPersistBackend site)
, RenderMessage site PWHashMessage
, RenderMessage site AFormMessage
, RenderAFormSite site
, RenderMessage site (ValueRequired site)
, Button site ButtonSubmit
) => PWHashAlgorithm -> AuthPlugin site
@ -60,7 +60,7 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard hashForm
((loginRes, _), _) <- runFormPost $ renderWForm FormLogin hashForm
tp <- getRouteToParent
resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
@ -81,7 +81,7 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard hashForm
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormLogin hashForm
let loginForm = wrapForm login FormSettings
{ formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR apName []

View File

@ -0,0 +1,17 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Control.Monad.Trans.Random.Instances
() where
-- import ClassyPrelude
import qualified Control.Monad.Trans.Random.Strict as Strict
import qualified Control.Monad.Trans.Random.Lazy as Lazy
import Control.Monad.Morph (MFunctor(..))
instance MFunctor (Strict.RandT g) where
hoist = Strict.mapRandT
instance MFunctor (Lazy.RandT g) where
hoist = Lazy.mapRandT

View File

@ -142,6 +142,11 @@ ordinalEN (toMessage -> numStr) = case lastChar of
where
lastChar = last <$> fromNullable numStr
notDE :: Bool -> Text
notDE = bool "nicht" ""
notEN :: Bool -> Text
notEN = bool "not" ""
-- | Convenience function for i18n messages definitions
maybeToMessage :: ToMessage m => Text -> Maybe m -> Text -> Text
@ -509,6 +514,14 @@ instance RenderMessage UniWorX RouteWorkflowScope where
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
instance RenderMessage UniWorX VolatileClusterSettingsKey where
renderMessage foundation ls = \case
ClusterVolatileWorkflowsEnabled -> mr MsgClusterVolatileWorkflowsEnabled
ClusterVolatileQuickActionsEnabled -> mr MsgClusterVolatileQuickActionsEnabled
where
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls
unRenderMessage' :: (Ord a, Finite a, RenderMessage master a) => (Text -> Text -> Bool) -> master -> Text -> [a]
unRenderMessage' cmp foundation inp = nubOrd $ do

View File

@ -775,38 +775,51 @@ defaultLinks = fmap catMaybes . mapM runMaybeT $ -- Define the menu items of the
, do
guardVolatile clusterVolatileWorkflowsEnabled
-- authCtx <- getAuthContext
-- (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,)
-- <$> haveTopWorkflowInstances
-- <*> haveTopWorkflowWorkflows
return NavHeader
{ navHeaderRole = NavHeaderPrimary
, navIcon = IconMenuWorkflows
, navLink = NavLink
{ navLabel = MsgMenuTopWorkflowInstanceList
, navRoute = TopWorkflowInstanceListR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
mUserId <- maybeAuthId
-- if | haveInstances -> return NavHeader
if | isJust mUserId -> return NavHeader
{ navHeaderRole = NavHeaderPrimary
, navIcon = IconMenuWorkflows
, navLink = NavLink
{ navLabel = MsgMenuTopWorkflowInstanceList
, navRoute = TopWorkflowInstanceListR
, navAccess' = NavAccessTrue
, navType = NavTypeLink { navModal = False }
, navQuick' = mempty
, navForceActive = False
}
}
-- | haveWorkflows -> return NavHeader
-- { navHeaderRole = NavHeaderPrimary
-- , navIcon = IconMenuWorkflows
-- , navLink = NavLink
-- { navLabel = MsgMenuTopWorkflowWorkflowListHeader
-- , navRoute = TopWorkflowWorkflowListR
-- , navAccess' = NavAccessTrue
-- , navType = NavTypeLink { navModal = False }
-- , navQuick' = mempty
-- , navForceActive = False
-- }
-- }
| otherwise -> mzero
-- -- authCtx <- getAuthContext
-- -- (haveInstances, haveWorkflows) <- lift . memcachedBy (Just . Right $ 2 * diffMinute) (NavCacheHaveTopWorkflowsInstances authCtx) . useRunDB $ (,)
-- -- <$> haveTopWorkflowInstances
-- -- <*> haveTopWorkflowWorkflows
-- mUserId <- maybeAuthId
-- -- if | haveInstances -> return NavHeader
-- if | isJust mUserId -> return NavHeader
-- { navHeaderRole = NavHeaderPrimary
-- , navIcon = IconMenuWorkflows
-- , navLink = NavLink
-- { navLabel = MsgMenuTopWorkflowInstanceList
-- , navRoute = TopWorkflowInstanceListR
-- , navAccess' = NavAccessTrue
-- , navType = NavTypeLink { navModal = False }
-- , navQuick' = mempty
-- , navForceActive = False
-- }
-- }
-- -- | haveWorkflows -> return NavHeader
-- -- { navHeaderRole = NavHeaderPrimary
-- -- , navIcon = IconMenuWorkflows
-- -- , navLink = NavLink
-- -- { navLabel = MsgMenuTopWorkflowWorkflowListHeader
-- -- , navRoute = TopWorkflowWorkflowListR
-- -- , navAccess' = NavAccessTrue
-- -- , navType = NavTypeLink { navModal = False }
-- -- , navQuick' = mempty
-- -- , navForceActive = False
-- -- }
-- -- }
-- | otherwise -> mzero
, return NavHeaderContainer
{ navHeaderRole = NavHeaderPrimary
, navLabel = SomeMessage MsgMenuAdminHeading

View File

@ -29,6 +29,7 @@ import Handler.Utils.Memcached
import qualified Data.Text as Text
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text.Lazy.Builder as LTB
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
@ -114,15 +115,16 @@ data MemcachedLimitKeyFavourites
deriving anyclass (Hashable, Binary)
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, MonadSecretBox (HandlerFor UniWorX)) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg = siteLayout . i18n
{-# DEPRECATED siteLayoutMsg' "Use siteLayoutMsg" #-}
siteLayoutMsg' :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg' :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, MonadSecretBox (HandlerFor UniWorX)) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg' = siteLayoutMsg
siteLayout :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend
, MonadSecretBox (HandlerFor UniWorX)
)
=> WidgetFor UniWorX () -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -130,6 +132,7 @@ siteLayout = siteLayout' . Just
siteLayout' :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend
, MonadSecretBox (HandlerFor UniWorX)
)
=> Maybe (WidgetFor UniWorX ()) -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -496,6 +499,12 @@ siteLayout' overrideHeading widget = do
toWidget $(juliusFile "templates/current-route.julius")
wellKnownHtmlLinks
whenM doFormHoneypots $ do
honeypotSecrets' <- liftHandler $ sortOn (view _2) . ifoldMap (\isHoneypot -> map (isHoneypot, ) . otoList) <$> honeypotSecrets
forM_ honeypotSecrets' $ \(isHoneypot, hpSecret) -> toWidget $ if
| isHoneypot -> CssBuilder . LTB.fromLazyText $ "[data-uw-field-display=\"" <> fromStrict hpSecret <> "\"]{display:none!important}"
| otherwise -> CssBuilder . LTB.fromLazyText $ "[data-uw-field-display=\"" <> fromStrict hpSecret <> "\"]{/*display:none!important*/}"
$(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")

View File

@ -257,6 +257,8 @@ upsertCampusUser upsertMode ldapData = do
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def

View File

@ -7,8 +7,6 @@ import Import
import Handler.Utils
import Handler.Utils.Communication
import qualified Data.Map as Map
import qualified Database.Esqueleto.Legacy as E
@ -17,8 +15,8 @@ getCCommR = postCCommR
postCCommR tid ssh csh = do
(cid, tuts, exams, sheets) <- runDB $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
tuts' <- selectKeysList [TutorialCourse ==. cid] []
tuts <- forM tuts' $ \tutid -> do
tuts'' <- selectKeysList [TutorialCourse ==. cid] []
tuts' <- forM tuts'' $ \tutid -> do
cID <- encrypt tutid
return ( RGTutorialParticipants cID
, E.from $ \(user `E.InnerJoin` participant) -> do
@ -26,6 +24,18 @@ postCCommR tid ssh csh = do
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
return user
)
let
tuts | length tuts' < 2 = tuts'
| otherwise = ( RGCourseParticipantsInTutorial
, E.from $ \(user `E.InnerJoin` participant) -> do
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
E.&&. participant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
E.&&. (E.exists . E.from $ \tutParticipant -> E.where_ $
tutParticipant E.^. TutorialParticipantUser E.==. user E.^. UserId
)
return user
) : tuts'
exams' <- selectKeysList [ExamCourse ==. cid] []
exams <- forM exams' $ \examid -> do
@ -55,7 +65,7 @@ postCCommR tid ssh csh = do
, crUltDest = SomeRoute $ CourseR tid ssh csh CCommR
, crJobs = crJobsCourseCommunication cid
, crTestJobs = crTestJobsCourseCommunication cid
, crRecipients = Map.fromList $
, crRecipients =
[ ( RGCourseParticipants
, E.from $ \(user `E.InnerJoin` participant) -> do
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
@ -69,14 +79,6 @@ postCCommR tid ssh csh = do
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
return user
)
, ( RGCourseCorrectors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
E.&&. user E.^. UserId E.==. corrector E.^. SheetCorrectorUser
return user
)
, ( RGCourseTutors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(tutorial `E.InnerJoin` tutor) -> do
@ -85,7 +87,16 @@ postCCommR tid ssh csh = do
E.&&. user E.^. UserId E.==. tutor E.^. TutorUser
return user
)
, ( RGCourseUnacceptedApplicants
, ( RGCourseCorrectors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
E.&&. user E.^. UserId E.==. corrector E.^. SheetCorrectorUser
return user
)
] ++ tuts ++ exams ++ sheets ++
[ ( RGCourseUnacceptedApplicants
, E.from $ \user -> do
E.where_ . E.exists . E.from $ \courseApplication ->
E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid
@ -96,7 +107,7 @@ postCCommR tid ssh csh = do
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
return user
)
] ++ tuts ++ exams ++ sheets
]
, crRecipientAuth = Just $ \uid -> do
cID <- encrypt uid
evalAccessDB (CourseR tid ssh csh $ CUserR cID) False

View File

@ -253,6 +253,12 @@ makeCourseTable colChoices psValidator' = do
Just needle -> (E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
E.||. (E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
E.||. (E.maybe (E.val mempty) (E.castString . esqueletoMarkupOutput) (course E.^. CourseDescription) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%))
, singletonMap "search-shorthand" . FilterColumn $ \(view queryCourse -> course) criterion -> case getLast (criterion :: Last Text) of
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
Just needle -> E.castString (course E.^. CourseShorthand) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)
, singletonMap "search-title" . FilterColumn $ \(view queryCourse -> course) criterion -> case getLast (criterion :: Last Text) of
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
Just needle -> E.castString (course E.^. CourseName) `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)
, singletonMap "allocation" . FilterColumn $ \row (criteria :: Set AllocationSearch) -> if
| Set.null criteria -> E.true
| otherwise -> flip E.any criteria $ \case
@ -267,6 +273,8 @@ makeCourseTable colChoices psValidator' = do
, pure $ prismAForm (singletonFilter "schoolshort" . maybePrism (_PathPiece . from _SchoolId)) mPrev $ aopt (hoistField lift schoolField) (fslI MsgTableCourseSchool)
, pure $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer)
, pure $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgFilterCourseSearch)
, pure $ prismAForm (singletonFilter "search-shorthand") mPrev $ aopt textField (fslI MsgFilterCourseSearchShorthand)
, pure $ prismAForm (singletonFilter "search-title") mPrev $ aopt textField (fslI MsgFilterCourseSearchTitle)
, pure $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgFilterCourseRegisterOpen)
, guardOn (is _Just muid)
$ prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgFilterCourseRegistered))

View File

@ -176,6 +176,7 @@ data UserTableCsv = UserTableCsv
, csvUserName :: UserDisplayName
, csvUserSex :: Maybe Sex
, csvUserMatriculation :: Maybe UserMatriculation
, csvUserEPPN :: Maybe UserEduPersonPrincipalName
, csvUserEmail :: UserEmail
, csvUserStudyFeatures :: UserTableStudyFeatures
, csvUserSubmissionGroup :: Maybe SubmissionGroupName
@ -194,6 +195,7 @@ instance Csv.ToNamedRecord UserTableCsv where
, "name" Csv..= csvUserName
, "sex" Csv..= csvUserSex
, "matriculation" Csv..= csvUserMatriculation
, "eduPersonPrincipalName" Csv..= csvUserEPPN
, "email" Csv..= csvUserEmail
, "study-features" Csv..= csvUserStudyFeatures
, "submission-group" Csv..= csvUserSubmissionGroup
@ -239,7 +241,7 @@ userTableCsvHeader :: Bool -> [Entity Tutorial] -> [Entity Sheet] -> UserCsvExpo
userTableCsvHeader showSex tuts sheets UserCsvExportData{..} = Csv.header $
[ "surname", "first-name", "name" ] ++
[ "sex" | showSex ] ++
[ "matriculation", "email", "study-features"] ++
[ "matriculation", "eduPersonPrincipalName", "email", "study-features"] ++
[ "tutorial" | hasEmptyRegGroup ] ++
map (encodeUtf8 . CI.foldedCase) regGroups ++
[ "exams", "registration" ] ++
@ -255,6 +257,7 @@ data UserTableJson = UserTableJson
, jsonUserName :: UserDisplayName
, jsonUserSex :: Maybe (Maybe Sex)
, jsonUserMatriculation :: Maybe UserMatriculation
, jsonUserEPPN :: Maybe UserEduPersonPrincipalName
, jsonUserEmail :: UserEmail
, jsonUserStudyFeatures :: UserTableStudyFeatures
, jsonUserSubmissionGroup :: Maybe SubmissionGroupName
@ -291,6 +294,7 @@ instance ToJSON UserTableJson where
, pure $ "name" JSON..= jsonUserName
, ("sex" JSON..=) <$> jsonUserSex
, ("matriculation" JSON..=) <$> jsonUserMatriculation
, ("eduPersonPrincipalName" JSON..=) <$> jsonUserEPPN
, pure $ "email" JSON..= jsonUserEmail
, ("study-features" JSON..=) <$> assertM' (views _Wrapped $ not . onull) jsonUserStudyFeatures
, ("submission-group" JSON..=) <$> jsonUserSubmissionGroup
@ -523,6 +527,7 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
<*> view (hasUser . _userDisplayName)
<*> view (hasUser . _userSex)
<*> view (hasUser . _userMatrikelnummer)
<*> view (hasUser . _userLdapPrimaryKey)
<*> view (hasUser . _userEmail)
<*> view _userStudyFeatures
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
@ -550,12 +555,13 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
repUserJson = C.foldMapM $ \(E.Value uid, res) -> Map.singleton <$> encrypt uid <*> mkUserTableJson res
where
mkUserTableJson res' = flip runReaderT res' $ UserTableJson
<$> view (hasUser . _userSurname)
<*> view (hasUser . _userFirstName)
<*> view (hasUser . _userDisplayName)
<$> view (hasUser . _userSurname)
<*> view (hasUser . _userFirstName)
<*> view (hasUser . _userDisplayName)
<*> views (hasUser . _userSex) (guardOn showSex)
<*> view (hasUser . _userMatrikelnummer)
<*> view (hasUser . _userEmail)
<*> view (hasUser . _userMatrikelnummer)
<*> view (hasUser . _userLdapPrimaryKey)
<*> view (hasUser . _userEmail)
<*> view _userStudyFeatures
<*> preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
<*> view _userTableRegistration

View File

@ -183,6 +183,7 @@ data ExamUserTableCsv = ExamUserTableCsv
, csvEUserFirstName :: Maybe Text
, csvEUserName :: Maybe Text
, csvEUserMatriculation :: Maybe Text
, csvEUserEPPN :: Maybe UserEduPersonPrincipalName
, csvEUserStudyFeatures :: UserTableStudyFeatures
, csvEUserOccurrence :: Maybe (CI Text)
, csvEUserExercisePoints :: Maybe (Maybe Points)
@ -203,6 +204,7 @@ instance ToNamedRecord ExamUserTableCsv where
, "first-name" Csv..= csvEUserFirstName
, "name" Csv..= csvEUserName
, "matriculation" Csv..= csvEUserMatriculation
, "eduPersonPrincipalName" Csv..= csvEUserEPPN
, "study-features" Csv..= csvEUserStudyFeatures
, "occurrence" Csv..= csvEUserOccurrence
] ++ catMaybes
@ -228,6 +230,7 @@ instance FromNamedRecord ExamUserTableCsv where
<*> csv .:?? "first-name"
<*> csv .:?? "name"
<*> csv .:?? "matriculation"
<*> csv .:?? "eduPersonPrincipalName"
<*> pure mempty
<*> csv .:?? "occurrence"
<*> fmap Just (csv .:?? "exercise-points")
@ -270,7 +273,7 @@ examUserTableCsvHeader :: ( MonoFoldable mono
=> SheetGradeSummary -> Bool -> mono -> Csv.Header
examUserTableCsvHeader allBoni doBonus pNames = Csv.header $
[ "surname", "first-name", "name"
, "matriculation"
, "matriculation", "eduPersonPrincipalName"
, "study-features"
, "course-note"
, "occurrence"
@ -608,6 +611,7 @@ postEUsersR tid ssh csh examn = do
<*> view (resultUser . _entityVal . _userFirstName . to Just)
<*> view (resultUser . _entityVal . _userDisplayName . to Just)
<*> view (resultUser . _entityVal . _userMatrikelnummer)
<*> view (resultUser . _entityVal . _userLdapPrimaryKey)
<*> view resultStudyFeatures
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
<*> fmap (bool (const Nothing) Just showPoints) (preview $ resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped)
@ -933,6 +937,7 @@ postEUsersR tid ssh csh examn = do
guessUser' ExamUserTableCsv{..} = do
let criteria = PredDNF . maybe Set.empty Set.singleton . fromNullable . Set.fromList . fmap PLVariable $ catMaybes
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
, GuessUserEduPersonPrincipalName <$> csvEUserEPPN
, GuessUserDisplayName <$> csvEUserName
, GuessUserSurname <$> csvEUserSurname
, GuessUserFirstName <$> csvEUserFirstName

View File

@ -216,24 +216,33 @@ embedRenderMessage ''UniWorX ''ExamUserAction id
data ExamUserActionData = ExamUserMarkSynchronisedData
newtype ExamUserCsvExportData = ExamUserCsvExportData
data ExamUserCsvExportData = ExamUserCsvExportData
{ csvEUserMarkSynchronised :: Bool
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
, csvEUserSetLabel :: Bool
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
-- | View a list of all users' grades that the current user has access to
getEGradesR, postEGradesR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEGradesR = postEGradesR
postEGradesR tid ssh csh examn = do
uid <- requireAuthId
Entity uid User{userCsvOptions=csvOpts} <- requireAuth
now <- liftIO getCurrentTime
((usersResult, examUsersTable), Entity eId Exam{examFinished}) <- runDB $ do
exam@(Entity eid Exam{..}) <- fetchExam tid ssh csh examn
Course{..} <- getJust examCourse
isLecturer <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR
isExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
userFunctions <- selectList [ UserFunctionUser ==. uid, UserFunctionFunction ==. SchoolExamOffice ] []
userCsvExportLabel' <- E.select . E.from $ \examOfficeLabel -> do
E.where_ $ maybe E.false (\expLbl -> examOfficeLabel E.^. ExamOfficeLabelName E.==. E.val expLbl) (csvExportLabel csvOpts)
E.&&. examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
return examOfficeLabel
let userCsvExportLabel = listToMaybe userCsvExportLabel'
let
participantLink :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (SomeRoute UniWorX)
participantLink partId = liftHandler $ do
@ -284,9 +293,15 @@ postEGradesR tid ssh csh examn = do
isSynced <- view . queryIsSynced $ E.val uid
lift $ do
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse)
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId)
E.&&. courseParticipant E.?. CourseParticipantState E.==. E.just (E.val CourseParticipantActive)
E.on $ E.maybe E.true (\cCourse ->
cCourse E.==. E.val examCourse
) (courseParticipant E.?. CourseParticipantCourse)
E.&&. E.maybe E.true (\cUser ->
cUser E.==. user E.^. UserId
) (courseParticipant E.?. CourseParticipantUser)
E.&&. E.maybe E.true (\cState ->
cState E.==. E.val CourseParticipantActive
) (courseParticipant E.?. CourseParticipantState)
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
E.&&. occurrence E.?. ExamOccurrenceId E.==. E.joinV (examRegistration E.?. ExamRegistrationOccurrence)
E.on $ examRegistration E.?. ExamRegistrationUser E.==. E.just (user E.^. UserId)
@ -332,7 +347,7 @@ postEGradesR tid ssh csh examn = do
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
lastChange <- view $ resultExamResult . _entityVal . _examResultLastChanged
user <- view $ resultUser . _entityVal
User{..} <- view $ resultUser . _entityVal
isSynced <- view resultIsSynced
let
hasSyncs = has folded syncs
@ -431,8 +446,17 @@ postEGradesR tid ssh csh examn = do
dbtCsvEncode = Just DBTCsvEncode
{ dbtCsvExportForm = ExamUserCsvExportData
<$> apopt checkBoxField (fslI MsgExamOfficeExamUserMarkSynchronisedCsv & setTooltip MsgExamOfficeExamUserMarkSynchronisedCsvTip) (Just False)
<*> bool
( pure False )
( maybe
(aforced checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip MsgExamOfficeLabelSetLabelOnExportForcedTip) False)
(\expLbl -> apopt checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip (MsgExamOfficeLabelSetLabelOnExportTip expLbl)) (Just True))
(examOfficeLabelName . entityVal <$> userCsvExportLabel)
)
isExamOffice
, dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do
when csvEUserMarkSynchronised $ markSynced k
when csvEUserSetLabel $ maybe (return ()) (\lbl -> void $ upsert (ExamOfficeExamLabel eid lbl) [ExamOfficeExamLabelLabel =. lbl]) (entityKey <$> userCsvExportLabel)
return $ ExamUserTableCsv
(row ^. resultUser . _entityVal . _userSurname)
(row ^. resultUser . _entityVal . _userFirstName)

View File

@ -1,7 +1,7 @@
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
module Handler.ExamOffice.Exams
( getEOExamsR
( getEOExamsR, postEOExamsR
) where
import Import
@ -16,46 +16,94 @@ import qualified Database.Esqueleto.Utils as E
import qualified Colonnade
import qualified Data.Conduit.Combinators as C
import qualified Data.Map as Map
import qualified Data.Set as Set
data ExamAction = ExamSetLabel | ExamRemoveLabel
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''ExamAction $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ExamAction id
data ExamActionData = ExamSetLabelData
{ easlNewLabel :: ExamOfficeLabelId
}
| ExamRemoveLabelData
deriving (Eq, Ord, Read, Show, Generic, Typeable)
data ExamsTableFilterProj = ExamsTableFilterProj
{ etProjFilterMayAccess :: Maybe Bool
{ etProjFilterMayAccess :: Maybe Bool
, etProjFilterHasResults :: Maybe Bool
, etProjFilterIsSynced :: Maybe Bool
, etProjFilterLabel :: Maybe (Either ExamOfficeExternalExamLabelId ExamOfficeExamLabelId)
, etProjFilterIsSynced :: Maybe Bool
}
instance Default ExamsTableFilterProj where
def = ExamsTableFilterProj
{ etProjFilterMayAccess = Nothing
{ etProjFilterMayAccess = Nothing
, etProjFilterHasResults = Nothing
, etProjFilterIsSynced = Nothing
, etProjFilterLabel = Nothing
, etProjFilterIsSynced = Nothing
}
makeLenses_ ''ExamsTableFilterProj
type ExamsTableExpr = ( E.SqlExpr (Maybe (Entity Exam))
`E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
`E.InnerJoin` E.SqlExpr (Maybe (Entity School))
type ExamsTableExpr = ( ( E.SqlExpr (Maybe (Entity Exam ))
`E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
`E.InnerJoin` E.SqlExpr (Maybe (Entity School))
)
`E.LeftOuterJoin`
( E.SqlExpr (Maybe (Entity ExamOfficeExamLabel))
`E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
)
)
`E.FullOuterJoin` ( E.SqlExpr (Maybe (Entity ExternalExam))
`E.LeftOuterJoin`
( E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel))
`E.InnerJoin` E.SqlExpr (Maybe (Entity ExamOfficeLabel))
)
)
`E.FullOuterJoin` E.SqlExpr (Maybe (Entity ExternalExam))
type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course, Entity School)
, Natural, Natural
type ExamsTableData = DBRow ( Either
( Entity ExternalExam
, Maybe (Entity ExamOfficeLabel)
)
( Entity Exam
, Entity Course
, Entity School
, Maybe (Entity ExamOfficeLabel)
)
, Maybe Natural
, Maybe Natural
)
queryExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Exam)))
queryExam = to $ $(E.sqlIJproj 3 1) . $(E.sqlFOJproj 2 1)
queryExam = to $ $(E.sqlIJproj 3 1) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Course)))
queryCourse = to $ $(E.sqlIJproj 3 2) . $(E.sqlFOJproj 2 1)
queryCourse = to $ $(E.sqlIJproj 3 2) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
querySchool :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity School)))
querySchool = to $ $(E.sqlIJproj 3 3) . $(E.sqlFOJproj 2 1)
querySchool = to $ $(E.sqlIJproj 3 3) . $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 1)
queryExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExamLabel)))
queryExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)
queryLabelExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
queryLabelExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 1)
queryExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExternalExam)))
queryExternalExam = to $(E.sqlFOJproj 2 2)
queryExternalExam = to $ $(E.sqlLOJproj 2 1) . $(E.sqlFOJproj 2 2)
queryExternalExamLabel :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeExternalExamLabel)))
queryExternalExamLabel = to $ $(E.sqlIJproj 2 1) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)
queryLabelExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExamOfficeLabel)))
queryLabelExternalExam = to $ $(E.sqlIJproj 2 2) . $(E.sqlLOJproj 2 2) . $(E.sqlFOJproj 2 2)
resultExam :: Traversal' ExamsTableData (Entity Exam)
resultExam = _dbrOutput . _1 . _Right . _1
@ -67,9 +115,12 @@ resultSchool :: Traversal' ExamsTableData (Entity School)
resultSchool = _dbrOutput . _1 . _Right . _3
resultExternalExam :: Traversal' ExamsTableData (Entity ExternalExam)
resultExternalExam = _dbrOutput . _1 . _Left
resultExternalExam = _dbrOutput . _1 . _Left . _1
resultSynchronised, resultResults :: Lens' ExamsTableData Natural
resultLabel :: Traversal' ExamsTableData (Maybe (Entity ExamOfficeLabel))
resultLabel = _dbrOutput . _1 . choosing _2 _4
resultSynchronised, resultResults :: Lens' ExamsTableData (Maybe Natural)
resultSynchronised = _dbrOutput . _2
resultResults = _dbrOutput . _3
@ -77,14 +128,45 @@ resultIsSynced :: Getter ExamsTableData Bool
resultIsSynced = to $ (>=) <$> view resultSynchronised <*> view resultResults
-- | List of all exams where the current user may (in her function as
-- exam-office) access users grades
getEOExamsR :: Handler Html
getEOExamsR = do
uid <- requireAuthId
-- | List of all exams where the current user may (in their function as exam-office) access users grades
getEOExamsR, postEOExamsR :: Handler Html
getEOExamsR = postEOExamsR
postEOExamsR = do
(uid, User{..}) <- requireAuthPair
now <- liftIO getCurrentTime
mr <- getMessageRender
getSynced <- lookupGetParam "synced" <&>
(\case
Just "yes" -> True
Just "no" -> False
_ -> userExamOfficeGetSynced
)
getLabels <- lookupGetParam "labels" <&>
(\case
Just "yes" -> True
Just "no" -> False
_ -> userExamOfficeGetLabels
)
(examsRes, examsTable) <- runDB $ do
let labelFilterNoLabelOption = Option
{ optionDisplay = mr MsgExamOfficeExamsNoLabel
, optionInternalValue = Nothing
, optionExternalValue = "no-label"
}
labelFilterOptions <- mkOptionList . (labelFilterNoLabelOption :) <$> do
labels <- E.select . E.from $ \examOfficeLabel -> do
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
E.orderBy [ E.asc $ examOfficeLabel E.^. ExamOfficeLabelName ]
return examOfficeLabel
return . flip map labels $ \(Entity lblId ExamOfficeLabel{..})
-> Option { optionDisplay = examOfficeLabelName
, optionInternalValue = Just lblId
, optionExternalValue = examOfficeLabelName
}
examsTable <- runDB $ do
let
examLink :: Course -> Exam -> SomeRoute UniWorX
examLink Course{..} Exam{..}
@ -98,43 +180,63 @@ getEOExamsR = do
externalExamLink ExternalExam{..}
= SomeRoute $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEGradesR
examActions :: Map ExamAction (AForm Handler ExamActionData)
examActions = Map.fromList $
bool mempty
[ ( ExamSetLabel, ExamSetLabelData
<$> apopt (selectField' Nothing . fmap (fmap entityKey) $ optionsPersist [ExamOfficeLabelUser ==. uid] [Asc ExamOfficeLabelName] examOfficeLabelName) (fslI MsgExamLabel) Nothing
)
, ( ExamRemoveLabel, pure ExamRemoveLabelData )
] getLabels
examsDBTable = DBTable{..}
where
dbtSQLQuery = runReaderT $ do
exam <- view queryExam
course <- view queryCourse
school <- view querySchool
externalExam <- view queryExternalExam
exam <- view queryExam
course <- view queryCourse
school <- view querySchool
mExamLabel <- view queryExamLabel
mLabelExam <- view queryLabelExam
externalExam <- view queryExternalExam
mExternalExamLabel <- view queryExternalExamLabel
mLabelExternalExam <- view queryLabelExternalExam
lift $ do
E.on $ mExternalExamLabel E.?. ExamOfficeExternalExamLabelLabel E.==. mLabelExternalExam E.?. ExamOfficeLabelId
E.on $ E.maybe E.true (\externalExamLabelExternalExamId ->
externalExam E.?. ExternalExamId E.==. E.just externalExamLabelExternalExamId
) (mExternalExamLabel E.?. ExamOfficeExternalExamLabelExternalExam)
E.on E.false
E.on $ school E.?. SchoolId E.==. course E.?. CourseSchool
E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId
E.on $ mExamLabel E.?. ExamOfficeExamLabelLabel E.==. mLabelExam E.?. ExamOfficeLabelId
E.on $ E.maybe E.true (\examLabelExamId ->
exam E.?. ExamId E.==. E.just examLabelExamId
) (mExamLabel E.?. ExamOfficeExamLabelExam)
E.on $ course E.?. CourseSchool E.==. school E.?. SchoolId
E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId
E.where_ $ (E.not_ (E.isNothing $ exam E.?. ExamId) E.&&. E.not_ (E.isNothing $ course E.?. CourseId) E.&&. E.isNothing (externalExam E.?. ExternalExamId))
E.||. ( E.isNothing (exam E.?. ExamId) E.&&. E.isNothing (course E.?. CourseId) E.&&. E.not_ (E.isNothing $ externalExam E.?. ExternalExamId))
E.where_ $ E.val (not getLabels) E.||. (
E.val getLabels
E.&&. E.maybe E.true (\labelExamUser ->
labelExamUser E.==. E.val uid
) (mLabelExam E.?. ExamOfficeLabelUser)
E.&&. E.maybe E.true (\labelExternalExamUser ->
labelExternalExamUser E.==. E.val uid
) (mLabelExternalExam E.?. ExamOfficeLabelUser)
)
return (exam, course, school, externalExam)
return (exam, course, school, mLabelExam, externalExam, mLabelExternalExam)
dbtRowKey = views ($(multifocusG 2) queryExam queryExternalExam) (bimap (E.?. ExamId) (E.?. ExternalExamId))
-- [ singletonMap "may-access" . FilterProjected $ \(Any b) r -> (== b) <$> if
-- | Just exam <- r ^? resultExam . _entityVal
-- , Just course <- r ^? resultCourse . _entityVal
-- -> hasReadAccessTo . urlRoute $ examLink course exam
-- | Just eexam <- r ^? resultExternalExam . _entityVal
-- -> hasReadAccessTo . urlRoute $ externalExamLink eexam :: DB Bool
-- | otherwise
-- -> return $ error "Got neither exam nor externalExam in result"
-- , singletonMap "has-results" . FilterProjected $ \(Any b) r -> (return $ b == (r ^. resultResults > 0) :: DB Bool)
-- , singletonMap "is-synced" . FilterProjected $ \(Any b) r -> (return $ b == (r ^. resultSynchronised >= r ^. resultResults) :: DB Bool)
-- ]
dbtProj :: _ ExamsTableData
dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do -- dbtProjSimple . runReaderT $ do
exam <- view $ _dbtProjRow . _dbrOutput . _1
course <- view $ _dbtProjRow . _dbrOutput . _2
school <- view $ _dbtProjRow . _dbrOutput . _3
externalExam <- view $ _dbtProjRow . _dbrOutput . _4
dbtProj = (views _dbtProjRow . set _dbrOutput) =<< do
exam <- view $ _dbtProjRow . _dbrOutput . _1
course <- view $ _dbtProjRow . _dbrOutput . _2
school <- view $ _dbtProjRow . _dbrOutput . _3
mExamLabel <- view $ _dbtProjRow . _dbrOutput . _4
externalExam <- view $ _dbtProjRow . _dbrOutput . _5
mExternalExamLabel <- view $ _dbtProjRow . _dbrOutput . _6
forMM_ (view $ _dbtProjFilter . _etProjFilterMayAccess) $ \b -> if
| Just (Entity _ exam') <- exam
@ -156,24 +258,41 @@ getEOExamsR = do
return $ ExternalExam.resultIsSynced (E.val uid) externalExamResult
getResults = getExamResults >> getExternalExamResults
foldResult (E.Value isSynced) = (Sum 1, guardMonoid isSynced $ Sum 1)
(Sum resultCount, Sum syncedCount) <- lift . lift . runConduit $ getResults .| C.foldMap foldResult
mCounts <- if getSynced
then do
(Sum resCount, Sum synCount) <- lift . lift . runConduit $ getResults .| C.foldMap foldResult
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) $ \b ->
guard $ b == (resCount > 0)
forMM_ (view $ _dbtProjFilter . _etProjFilterIsSynced) $ \b ->
guard $ b == (synCount >= resCount)
return $ Just (resCount, synCount)
else do
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) guard
return Nothing
forMM_ (view $ _dbtProjFilter . _etProjFilterHasResults) $ \b ->
guard $ b == (resultCount > 0)
forMM_ (view $ _dbtProjFilter . _etProjFilterIsSynced) $ \b ->
guard $ b == (syncedCount >= resultCount)
case (exam, course, school, externalExam) of
(Just exam', Just course', Just school', Nothing) -> return
(Right (exam', course', school'), syncedCount, resultCount)
(Nothing, Nothing, Nothing, Just externalExam') -> return
(Left externalExam', syncedCount, resultCount)
case (exam, course, school, mExamLabel, externalExam, mExternalExamLabel) of
(Just exam', Just course', Just school', mExamLabel', Nothing, Nothing) -> return
(Right (exam', course', school', mExamLabel'), snd <$> mCounts, fst <$> mCounts)
(Nothing, Nothing, Nothing, Nothing, Just externalExam', mExternalExamLabel') -> return
(Left (externalExam', mExternalExamLabel'), snd <$> mCounts, fst <$> mCounts)
_other -> return $ error "Got exam & externalExam in same result"
colLabel = Colonnade.singleton (fromSortable . Sortable (Just "label") $ i18nCell MsgTableExamLabel) $ \x -> flip runReader x $ do
mLabel <- preview resultLabel
-- TODO: use select frontend util
if
| Just (Just (Entity _ ExamOfficeLabel{..})) <- mLabel
-> return $ cell $(widgetFile "widgets/exam-office-label")
| otherwise -> return $ cell mempty
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
mExam <- preview resultExam
mSchool <- preview resultSchool
mExam <- preview resultExam
mSchool <- preview resultSchool
mSynced <- view resultSynchronised
mResults <- view resultResults
if
| Just (Entity _ Exam{examClosed, examFinished}) <- mExam
@ -182,12 +301,10 @@ getEOExamsR = do
(NTop examClosed > NTop (Just now))
$ is _ExamCloseSeparate schoolExamCloseMode
-> return . cell $ toWidget iconNew
| otherwise
| Just synced <- mSynced
, Just results <- mResults
-> do
synced <- view resultSynchronised
results <- view resultResults
isSynced <- view resultIsSynced
return $ cell
[whamlet|
$newline never
@ -199,11 +316,14 @@ getEOExamsR = do
& cellAttrs <>~ [ ("class", "heated")
, ("style", [st|--hotness: #{tshow (heat results synced)}|])
]
| otherwise -> return $ cell mempty
dbtColonnade :: Colonnade Sortable _ _
dbtColonnade = mconcat
[ colSynced
[ bool mempty (dbSelect (applying _2) id $ \DBRow{ dbrOutput=(ex,_,_) } -> return $ bimap (\(Entity eeId _,_) -> eeId) (\(Entity eId _,_,_,_) -> eId) ex) (not $ Map.null examActions)
, bool mempty colLabel getLabels
, bool mempty colSynced getSynced
, maybeAnchorColonnade ( runMaybeT $ mpreview ($(multifocusG 2) (pre $ resultCourse . _entityVal) (pre $ resultExam . _entityVal) . to (uncurry $ liftA2 examLink) . _Just)
<|> mpreviews (resultExternalExam . _entityVal) externalExamLink
)
@ -216,12 +336,20 @@ getEOExamsR = do
, emptyOpticColonnade (resultCourse . _entityVal . _courseSchool <> resultExternalExam . _entityVal . _externalExamSchool) colSchool
, emptyOpticColonnade (resultCourse . _entityVal . _courseTerm <> resultExternalExam . _entityVal . _externalExamTerm) colTermShort
]
dbtSorting = mconcat
dbtSorting = mconcat $
bool mempty
[ singletonMap "label-prio" $
SortProjected . comparing $ (fmap . fmap $ examOfficeLabelPriority . entityVal) <$> preview resultLabel
, singletonMap "label-status" $
SortProjected . comparing $ (fmap . fmap $ examOfficeLabelStatus . entityVal) <$> preview resultLabel
] getLabels <>
bool mempty
[ singletonMap "synced" $
SortProjected . comparing $ ((/) `on` toRational) <$> view resultSynchronised <*> view resultResults
SortProjected . comparing $ ((/) `on` toRational . fromMaybe 0) <$> view resultSynchronised <*> view resultResults
, singletonMap "is-synced" $
SortProjected . comparing $ (>=) <$> view resultSynchronised <*> view resultResults
, sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
] getSynced <>
[ sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
, sortExamTime (queryExam . $(multifocusG 2) (to $ E.joinV . (E.?. ExamStart)) (to $ E.joinV . (E.?. ExamEnd)))
, sortExamFinished (queryExam . to (E.joinV . (E.?. ExamFinished)))
, sortExamClosed (queryExam . to (E.joinV . (E.?. ExamClosed)))
@ -231,31 +359,67 @@ getEOExamsR = do
]
dbtFilter = mconcat
[ singletonMap "may-access" . FilterProjected $ (_etProjFilterMayAccess ?~) . getAny
[ singletonMap "may-access" . FilterProjected $ (_etProjFilterMayAccess ?~) . getAny
, singletonMap "has-results" . FilterProjected $ (_etProjFilterHasResults ?~) . getAny
, singletonMap "is-synced" . FilterProjected $ (_etProjFilterIsSynced ?~) . getAny
]
dbtFilterUI = mconcat
[ flip (prismAForm $ singletonFilter "is-synced" . maybePrism _PathPiece) $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamSynchronised)
, singletonMap "is-synced" . FilterProjected $ (_etProjFilterIsSynced ?~) . getAny
, singletonMap "label" . FilterColumn . E.mkExactFilter $ views queryLabelExam (E.?. ExamOfficeLabelId)
]
dbtFilterUI mPrev = mconcat $
[ prismAForm (singletonFilter "label" . maybePrism _PathPiece) mPrev $ aopt (selectField' (Just $ SomeMessage MsgTableNoFilter) $ return labelFilterOptions) (fslI MsgExamLabel)
| getLabels ] <>
[ prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamSynchronised)
| getSynced ]
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
dbtParams = def
dbtParams = DBParamsForm
{ dbParamsFormMethod = POST
, dbParamsFormAction = Just . SomeRoute $ ExamOfficeR EOExamsR
, dbParamsFormAttrs = []
, dbParamsFormSubmit = FormSubmit
, dbParamsFormAdditional
= renderAForm FormStandard
$ (, mempty) . First . Just
<$> multiActionA examActions (fslI MsgTableAction) Nothing
, dbParamsFormEvaluate = liftHandler . runFormPost
, dbParamsFormResult = id
, dbParamsFormIdent = def
}
dbtIdent :: Text
dbtIdent = "exams"
dbtCsvEncode = noCsvEncode
dbtCsvDecode = Nothing
dbtExtraReps = []
examsDBTableValidator = def
& defaultSorting [SortAscBy "is-synced", SortAscBy "exam-time"]
& defaultSorting (bool mempty [SortDescBy "label-prio", SortAscBy "label-status"] getLabels <> bool mempty [SortAscBy "is-synced"] getSynced <> [SortAscBy "exam-time"])
& forceFilter "may-access" (Any True)
& forceFilter "has-results" (Any True)
dbTableWidget' examsDBTableValidator examsDBTable
postprocess :: FormResult (First ExamActionData , DBFormResult (Either ExternalExamId ExamId) Bool (DBRow (Either (Entity ExternalExam, Maybe (Entity ExamOfficeLabel)) (Entity Exam, Entity Course, Entity School, Maybe (Entity ExamOfficeLabel)), Maybe Natural, Maybe Natural)))
-> FormResult ( ExamActionData , Set (Either ExternalExamId ExamId))
postprocess (FormFailure errs) = FormFailure errs
postprocess FormMissing = FormMissing
postprocess (FormSuccess (First mExamActionData, examRes))
| Just act <- mExamActionData = FormSuccess . (act,) . Map.keysSet . Map.filter id $ getDBFormResult (const False) examRes
| otherwise = FormMissing
over _1 postprocess <$> dbTable examsDBTableValidator examsDBTable
formResult examsRes $ \(examAction, exams) -> case examAction of
ExamSetLabelData{..} -> do
runDB . forM_ (Set.toList exams) $ either (\eeid -> void $ upsert (ExamOfficeExternalExamLabel eeid easlNewLabel) [ExamOfficeExternalExamLabelLabel =. easlNewLabel]) (\eid -> void $ upsert (ExamOfficeExamLabel eid easlNewLabel) [ExamOfficeExamLabelLabel =. easlNewLabel])
addMessageI Success $ MsgExamLabelsSet (Set.size exams)
redirect $ ExamOfficeR EOExamsR
ExamRemoveLabelData -> do
runDB . forM_ (Set.toList exams) $ either
(\eeId -> E.delete . E.from $ \extExLabel -> E.where_ (extExLabel E.^. ExamOfficeExternalExamLabelExternalExam E.==. E.val eeId))
(\eId -> E.delete . E.from $ \exLabel -> E.where_ (exLabel E.^. ExamOfficeExamLabelExam E.==. E.val eId))
addMessageI Success $ MsgExamLabelsRemoved (Set.size exams)
redirect $ ExamOfficeR EOExamsR
siteLayoutMsg MsgHeadingExamList $ do
setTitleI MsgHeadingExamList

View File

@ -35,7 +35,7 @@ helpForm mReferer mUid = renderWForm FormStandard $ do
let defaultActions =
[ ( HIEmail
, Left . Just <$> (Address <$> aopt textField (fslpI MsgHelpName $ mr MsgHelpName) Nothing <*> apreq emailField (fslpI MsgHelpEmail $ mr MsgEMail) Nothing)
, Left . Just <$> (Address <$> aopt textField (fslpI MsgHelpName (mr MsgHelpName) & addAttr "autocomplete" "name") Nothing <*> apreq emailField (fslpI MsgHelpEmail (mr MsgEMail) & addAttr "autocomplete" "email") Nothing)
)
, ( HIAnonymous
, pure $ Left Nothing

View File

@ -73,15 +73,17 @@ newsSystemMessages = do
return $ guardOn (not hidden || showHidden) (smId, sm, trans, hidden)
(messages', Any anyHidden) <- liftHandler . runDB . runConduit . C.runWriterLC $
transPipe lift (selectKeys [] [])
.| C.filterM (lift . hasReadAccessTo . MessageR <=< encrypt)
.| transPipe lift (C.mapMaybeM $ \smId -> fmap (\args@(sm, _) -> (smId, sm, systemMessageToTranslation smId args)) <$> getSystemMessage smId)
.| C.filter (\(_, SystemMessage{..}, _) -> NTop systemMessageFrom <= NTop (Just now) && NTop (Just now) < NTop systemMessageTo)
.| C.mapMaybeM checkHidden
.| C.iterM (\(smId, _, _, _) -> tellShown smId)
.| C.mapM (\(smId, sm@SystemMessage{..}, trans, hidden) -> (sm, trans, hidden,,) <$> formatTime SelFormatDateTime (maybe id max systemMessageFrom systemMessageLastChanged) <*> mkHideForm smId sm)
.| C.consume
(messages', Any anyHidden) <- liftHandler . runDB $ do
volatileClusterConfig <- selectList [] []
runConduit . C.runWriterLC $
transPipe lift (selectKeys [] [])
.| C.filterM (lift . hasReadAccessTo . MessageR <=< encrypt)
.| transPipe lift (C.mapMaybeM $ \smId -> fmap (\args@(sm, _) -> (smId, sm, systemMessageToTranslation smId args)) <$> getSystemMessage smId)
.| C.filter (\(_, SystemMessage{..}, _) -> NTop systemMessageFrom <= NTop (Just now) && NTop (Just now) < NTop systemMessageTo && all (\(k,v) -> (k,v) `elem` ((\VolatileClusterConfig{..} -> (volatileClusterConfigSetting, volatileClusterConfigValue)) . entityVal <$> volatileClusterConfig)) (Set.toList systemMessageOnVolatileClusterSettings))
.| C.mapMaybeM checkHidden
.| C.iterM (\(smId, _, _, _) -> tellShown smId)
.| C.mapM (\(smId, sm@SystemMessage{..}, trans, hidden) -> (sm, trans, hidden,,) <$> formatTime SelFormatDateTime (maybe id max systemMessageFrom systemMessageLastChanged) <*> mkHideForm smId sm)
.| C.consume
let messages = sortOn (\(SystemMessage{..}, _, _, _, _) -> (Down systemMessageManualPriority, Down $ maybe id max systemMessageFrom systemMessageLastChanged, systemMessageSeverity)) messages'
hiddenUrl <- toTextUrl (NewsR, [(toPathPiece GetHidden, "")])

View File

@ -31,6 +31,20 @@ import Jobs
import Foundation.Yesod.Auth (updateUserLanguage)
data ExamOfficeSettings
= ExamOfficeSettings
{ eosettingsGetSynced :: Bool
, eosettingsGetLabels :: Bool
, eosettingsLabels :: EOLabels
}
type EOLabelData
= ( ExamOfficeLabelName
, MessageStatus -- status
, Int -- priority; also used for label ordering
)
type EOLabels = Map (Either ExamOfficeLabelName ExamOfficeLabelId) EOLabelData
data SettingsForm = SettingsForm
{ stgDisplayName :: UserDisplayName
, stgDisplayEmail :: UserEmail
@ -43,6 +57,7 @@ data SettingsForm = SettingsForm
, stgDownloadFiles :: Bool
, stgWarningDays :: NominalDiffTime
, stgShowSex :: Bool
, stgExamOfficeSettings :: ExamOfficeSettings
, stgSchools :: Set SchoolId
, stgNotificationSettings :: NotificationSettings
, stgAllocationNotificationSettings :: Map AllocationId (Maybe Bool)
@ -115,6 +130,7 @@ makeSettingForm template html = do
& setTooltip MsgWarningDaysTip
) (stgWarningDays <$> template)
<*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template)
<*> examOfficeForm (stgExamOfficeSettings <$> template)
<* aformSection MsgFormNotifications
<*> schoolsForm (stgSchools <$> template)
<*> notificationForm (stgNotificationSettings <$> template)
@ -311,6 +327,101 @@ allocationNotificationForm = maybe (pure mempty) allocationNotificationForm' . (
where funcForm' forms = funcForm forms (fslI MsgFormAllocationNotifications & setTooltip MsgFormAllocationNotificationsTip) False
examOfficeForm :: Maybe ExamOfficeSettings -> AForm Handler ExamOfficeSettings
examOfficeForm template = wFormToAForm $ do
(_uid, User{userExamOfficeGetSynced,userExamOfficeGetLabels}) <- requireAuthPair
currentRoute <- fromMaybe (error "examOfficeForm called from 404-handler") <$> liftHandler getCurrentRoute
mr <- getMessageRender
let
userExamOfficeLabels :: EOLabels
userExamOfficeLabels = maybe mempty eosettingsLabels template
eoLabelsForm :: AForm Handler EOLabels
eoLabelsForm = wFormToAForm $ do
let
miAdd :: ListPosition
-> Natural
-> ListLength
-> (Text -> Text)
-> FieldView UniWorX
-> Maybe
(Form (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)
-> FormResult (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)))
)
miAdd _ _ _ nudge submitView = Just $ \csrf -> do
(addRes, addView) <- mpreq textField (fslI MsgExamOfficeLabelName & addName (nudge "name")) Nothing
let
addRes' = addRes <&> \nLabel oldData@(maybe 0 (succ . fst) . Map.lookupMax -> kStart) -> if
| Set.member (Left nLabel) . Set.fromList $ Map.elems oldData
-> FormFailure [mr MsgExamOfficeLabelAlreadyExists]
| otherwise
-> FormSuccess $ Map.singleton kStart (Left nLabel)
return (addRes', $(widgetFile "profile/exam-office-labels/add"))
miCell :: ListPosition
-> Either ExamOfficeLabelName ExamOfficeLabelId
-> Maybe EOLabelData
-> (Text -> Text)
-> Form EOLabelData
miCell _ eoLabel initRes nudge csrf = do
labelIdent <- case eoLabel of
Left lblName -> return lblName
Right lblId -> do
ExamOfficeLabel{examOfficeLabelName} <- liftHandler . runDB $ getJust lblId
return examOfficeLabelName
(statusRes, statusView) <- mreq (selectField optionsFinite) (fslI MsgExamOfficeLabelStatus & addName (nudge "status")) ((\(_,x,_) -> x) <$> initRes)
(priorityRes, priorityView) <- mreq intField (fslI MsgExamOfficeLabelPriority & addName (nudge "priority")) (((\(_,_,x) -> x) <$> initRes) <|> Just 0)
let
res :: FormResult EOLabelData
res = (,,) <$> FormSuccess labelIdent <*> statusRes <*> priorityRes
return (res, $(widgetFile "profile/exam-office-labels/cell"))
miDelete :: Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId)
-> ListPosition
-> MaybeT (MForm Handler) (Map ListPosition ListPosition)
miDelete = miDeleteList
miAddEmpty :: ListPosition
-> Natural
-> ListLength
-> Set ListPosition
miAddEmpty _ _ _ = Set.empty
miButtonAction :: forall p.
PathPiece p
=> p
-> Maybe (SomeRoute UniWorX)
miButtonAction frag = Just . SomeRoute $ currentRoute :#: frag
miLayout :: ListLength
-> Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId, FormResult EOLabelData)
-> Map ListPosition Widget
-> Map ListPosition (FieldView UniWorX)
-> Map (Natural, ListPosition) Widget
-> Widget
miLayout lLength _ cellWdgts delButtons addWdgets = $(widgetFile "profile/exam-office-labels/layout")
miIdent :: Text
miIdent = "exam-office-labels"
filledData :: Maybe (Map ListPosition (Either ExamOfficeLabelName ExamOfficeLabelId, EOLabelData))
filledData = Just . Map.fromList . zip [0..] $ Map.toList userExamOfficeLabels
fmap (Map.fromList . Map.elems) <$> massInputW MassInput{..} (fslI MsgExamOfficeLabels & setTooltip MsgExamOfficeLabelsTip) False filledData
userIsExamOffice <- liftHandler . hasReadAccessTo $ ExamOfficeR EOExamsR
if userIsExamOffice
then
aFormToWForm $ ExamOfficeSettings
<$ aformSection MsgFormExamOffice
<*> apopt checkBoxField (fslI MsgExamOfficeGetSynced & setTooltip MsgExamOfficeGetSyncedTip) (eosettingsGetSynced <$> template)
<*> apopt checkBoxField (fslI MsgExamOfficeGetLabels & setTooltip MsgExamOfficeGetLabelsTip) (eosettingsGetLabels <$> template)
<*> eoLabelsForm
else
return . pure . fromMaybe (ExamOfficeSettings userExamOfficeGetSynced userExamOfficeGetLabels userExamOfficeLabels) $ template
validateSettings :: User -> FormValidator SettingsForm Handler ()
validateSettings User{..} = do
userDisplayName' <- use _stgDisplayName
@ -342,12 +453,15 @@ getProfileR, postProfileR :: Handler Html
getProfileR = postProfileR
postProfileR = do
(uid, user@User{..}) <- requireAuthPair
userSchools <- fmap (setOf $ folded . _Value) . runDB . E.select . E.from $ \school -> do
E.where_ . E.exists . E.from $ \userSchool ->
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
return $ school E.^. SchoolId
(userSchools, userExamOfficeLabels) <- runDB $ do
userSchools <- fmap (setOf $ folded . _Value) . E.select . E.from $ \school -> do
E.where_ . E.exists . E.from $ \userSchool ->
E.where_ $ E.not_ (userSchool E.^. UserSchoolIsOptOut)
E.&&. userSchool E.^. UserSchoolUser E.==. E.val uid
E.&&. userSchool E.^. UserSchoolSchool E.==. school E.^. SchoolId
return $ school E.^. SchoolId
userExamOfficeLabels <- selectList [ ExamOfficeLabelUser ==. uid ] []
return (userSchools, userExamOfficeLabels)
allocs <- runDB $ getAllocationNotifications uid
let settingsTemplate = Just SettingsForm
{ stgDisplayName = userDisplayName
@ -363,6 +477,11 @@ postProfileR = do
, stgNotificationSettings = userNotificationSettings
, stgWarningDays = userWarningDays
, stgShowSex = userShowSex
, stgExamOfficeSettings = ExamOfficeSettings
{ eosettingsGetSynced = userExamOfficeGetSynced
, eosettingsGetLabels = userExamOfficeGetLabels
, eosettingsLabels = flip foldMap userExamOfficeLabels $ \(Entity eolid ExamOfficeLabel{..}) -> Map.singleton (Right eolid) (examOfficeLabelName,examOfficeLabelStatus,examOfficeLabelPriority)
}
, stgAllocationNotificationSettings = allocs
}
((res,formWidget), formEnctype) <- runFormPost . validateForm (validateSettings user) . identifyForm ProfileSettings $ makeSettingForm settingsTemplate
@ -381,6 +500,8 @@ postProfileR = do
, UserWarningDays =. stgWarningDays
, UserNotificationSettings =. stgNotificationSettings
, UserShowSex =. stgShowSex
, UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced)
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
] ++ [ UserDisplayEmail =. stgDisplayEmail | userDisplayEmail == stgDisplayEmail ]
setAllocationNotifications uid stgAllocationNotificationSettings
updateFavourites Nothing
@ -406,6 +527,26 @@ postProfileR = do
}
[ UserSchoolIsOptOut =. True
]
let
oldExamLabels = userExamOfficeLabels
newExamLabels = stgExamOfficeSettings & eosettingsLabels
forM_ oldExamLabels $ \(Entity eolid ExamOfficeLabel{..}) -> unless (Right eolid `Map.member` newExamLabels || Left examOfficeLabelName `Map.member` newExamLabels) $ do
E.delete . E.from $ \examOfficeExternalExamLabel -> E.where_ $ examOfficeExternalExamLabel E.^. ExamOfficeExternalExamLabelLabel E.==. E.val eolid
E.delete . E.from $ \examOfficeExamLabel -> E.where_ $ examOfficeExamLabel E.^. ExamOfficeExamLabelLabel E.==. E.val eolid
when (csvExportLabel userCsvOptions == Just examOfficeLabelName) $
update uid [ UserCsvOptions =. userCsvOptions { csvExportLabel = Nothing } ]
delete eolid
forM_ (Map.toList newExamLabels) $ \(eoLabelIdent, (examOfficeLabelName, examOfficeLabelStatus, examOfficeLabelPriority)) -> case eoLabelIdent of
Left _ -> void $ upsert ExamOfficeLabel{ examOfficeLabelUser=uid, .. }
[ ExamOfficeLabelName =. examOfficeLabelName
, ExamOfficeLabelStatus =. examOfficeLabelStatus
, ExamOfficeLabelPriority =. examOfficeLabelPriority
]
Right lblId -> update lblId
[ ExamOfficeLabelName =. examOfficeLabelName
, ExamOfficeLabelStatus =. examOfficeLabelStatus
, ExamOfficeLabelPriority =. examOfficeLabelPriority
]
addMessageI Success MsgSettingsUpdate
redirect $ ProfileR :#: ProfileSettings
@ -454,7 +595,6 @@ getProfileDataR = do
makeProfileData :: Entity User -> DB Widget
makeProfileData (Entity uid User{..}) = do
-- MsgRenderer mr <- getMsgRenderer
functions <- Map.fromListWith Set.union . map (\(Entity _ UserFunction{..}) -> (userFunctionFunction, Set.singleton userFunctionSchool)) <$> selectList [UserFunctionUser ==. uid] []
lecture_corrector <- E.select $ E.distinct $ E.from $ \(sheet `E.InnerJoin` corrector `E.InnerJoin` course) -> do
E.on $ sheet E.^. SheetCourse E.==. course E.^. CourseId
@ -486,8 +626,8 @@ makeProfileData (Entity uid User{..}) = do
-- | Table listing all courses that the given user is a lecturer for
mkOwnedCoursesTable :: UserId -> DB (Bool, Widget)
-- Table listing all courses that the given user is a lecturer for
mkOwnedCoursesTable =
let dbtIdent = "courseOwnership" :: Text
dbtStyle = def
@ -537,9 +677,8 @@ mkOwnedCoursesTable =
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
-- | Table listing all courses that the given user is enrolled in
mkEnrolledCoursesTable :: UserId -> DB Widget
-- Table listing all courses that the given user is enrolled in
mkEnrolledCoursesTable =
let withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity CourseParticipant)) -> a)
@ -590,9 +729,8 @@ mkEnrolledCoursesTable =
}
-- | Table listing all submissions for the given user
mkSubmissionTable :: UserId -> DB Widget
-- Table listing all submissions for the given user
mkSubmissionTable =
let dbtIdent = "submissions" :: Text
dbtStyle = def
@ -676,9 +814,8 @@ mkSubmissionTable =
-- return $ dbTableWidget' validator $ DBTable {..}
-- | Table listing all submissions for the given user
mkSubmissionGroupTable :: UserId -> DB Widget
-- Table listing all submissions for the given user
mkSubmissionGroupTable =
let dbtIdent = "subGroups" :: Text
dbtStyle = def
@ -733,13 +870,10 @@ mkSubmissionGroupTable =
in dbTableWidget' validator DBTable{..}
mkCorrectionsTable :: UserId -> DB Widget
-- Table listing sum of corrections made by the given user per sheet
mkCorrectionsTable =
let dbtIdent = "corrections" :: Text
dbtStyle = def
-- TODO Continue here
withType :: ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
-> ((E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity SheetCorrector))->a)
withType = id
@ -929,8 +1063,14 @@ getCsvOptionsR = postCsvOptionsR
postCsvOptionsR = do
Entity uid User{userCsvOptions} <- requireAuth
userIsExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
examOfficeLabels <- if not userIsExamOffice then return mempty else runDB . E.select . E.from $ \examOfficeLabel -> do
E.where_ $ examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
E.orderBy [ E.asc (examOfficeLabel E.^. ExamOfficeLabelName) ]
return $ examOfficeLabel E.^. ExamOfficeLabelName
((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
csvOptionsForm (Just userCsvOptions)
csvOptionsForm (Just userCsvOptions) (Set.fromList $ E.unValue <$> examOfficeLabels)
formResultModal optionsRes CsvOptionsR $ \opts -> do
lift . runDB $ update uid [ UserCsvOptions =. opts ]

View File

@ -419,7 +419,6 @@ submissionHelper tid ssh csh shn mcid = do
isParticipant = E.exists . E.from $ \courseParticipant -> do
E.where_ $ user E.^. UserId E.==. courseParticipant E.^. CourseParticipantUser
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. E.val sheetCourse
E.&&. courseParticipant E.^. CourseParticipantState E.==. E.val CourseParticipantActive
hasSubmitted = E.exists . E.from $ \(submissionUser `E.InnerJoin` submission) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. user E.^. UserId

View File

@ -781,6 +781,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI' mCSVSettings psValida
data ActionCorrections = CorrDownload
| CorrSetCorrector
| CorrAutoSetCorrector
| CorrSetCorrectionsDone
| CorrDelete
deriving (Eq, Ord, Read, Show, Enum, Bounded)
@ -793,6 +794,7 @@ embedRenderMessage ''UniWorX ''ActionCorrections id
data ActionCorrectionsData = CorrDownloadData SubmissionDownloadAnonymous SubmissionFileType
| CorrSetCorrectorData (Maybe UserId)
| CorrAutoSetCorrectorData SheetId
| CorrSetCorrectionsDoneData Bool
| CorrDeleteData
correctionsR :: CorrectionTableWhere -> _ -> _ -> Maybe CorrectionTableCsvSettings -> _ -> Map ActionCorrections (AForm (HandlerFor UniWorX) ActionCorrectionsData) -> Handler TypedContent
@ -927,6 +929,15 @@ correctionsR' whereClause displayColumns dbtFilterUI csvSettings psValidator act
unassigned' <- forM (Set.toList stillUnassigned) $ \sid -> encrypt sid :: DB CryptoFileNameSubmission
addMessage Warning =<< withUrlRenderer ($(ihamletFile "templates/messages/submissionsNotAssignedAuto.hamlet") mr)
redirect currentRoute
(CorrSetCorrectionsDoneData isDone, subs') -> do
now <- liftIO getCurrentTime
subs <- mapM decrypt $ Set.toList subs'
runDB $ do
_ <- updateWhere [SubmissionId <-. subs]
[SubmissionRatingTime =. bool Nothing (Just now) isDone]
addMessageI Success $ MsgSetCorrectionsDone isDone
auditAllSubEdit subs
redirect currentRoute
(CorrDeleteData, subs) -> do
subs' <- Set.fromList <$> forM (Set.toList subs) decrypt -- Set is not traversable
getDeleteR (submissionDeleteRoute subs')
@ -997,6 +1008,12 @@ assignAction selId = ( CorrSetCorrector
fmap CorrSetCorrectorData <$> (traverse.traverse) decrypt cId
)
setCorrectionsDoneAction :: ActionCorrections'
setCorrectionsDoneAction = ( CorrSetCorrectionsDone
, CorrSetCorrectionsDoneData
<$> apopt checkBoxField (fslI MsgCorrSetCorrectionsDone) (Just True)
)
autoAssignAction :: SheetId -> ActionCorrections'
autoAssignAction shid = ( CorrAutoSetCorrector
, pure $ CorrAutoSetCorrectorData shid
@ -1023,7 +1040,7 @@ postCorrectionsR = do
, colAssigned
, colRating
, colRated
] -- Continue here
]
filterUI = Just $ mconcat
[ filterUIPseudonym
, filterUICourse courseOptions
@ -1046,7 +1063,7 @@ postCorrectionsR = do
psValidator = def
& restrictCorrector
& restrictAnonymous
& defaultSorting [SortDescBy "ratingtime", SortAscBy "assignedtime" ]
& defaultSorting [SortAscBy "assignedtime", SortDescBy "ratingtime"]
& defaultFilter (singletonMap "israted" [toPathPiece False])
csvSettings = Just CorrectionTableCsvSettings
@ -1109,6 +1126,7 @@ postCCorrectionsR tid ssh csh = do
correctionsR whereClause colonnade filterUI csvSettings psValidator $ Map.fromList
[ downloadAction
, assignAction (Left cid)
, setCorrectionsDoneAction
, deleteAction
]
@ -1157,5 +1175,6 @@ postSSubsR tid ssh csh shn = do
[ downloadAction
, assignAction (Right shid)
, autoAssignAction shid
, setCorrectionsDoneAction
, deleteAction
]

View File

@ -7,25 +7,49 @@ module Handler.SystemMessage
import Import
import qualified Data.Map.Lazy as Map
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HashMap
import qualified Data.List.NonEmpty as NonEmpty
import Handler.Utils
import Handler.Utils.News
import qualified Data.HashMap.Strict as HashMap
import Data.Map ((!))
import qualified Data.Map.Lazy as Map
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Set as Set
import qualified Data.Text as Text (intercalate)
import qualified Database.Esqueleto.Legacy as E
-- htmlField' moved to Handler.Utils.Form/Fields
invalidateVisibleSystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX)
=> m ()
invalidateVisibleSystemMessages
= memcachedByInvalidate AuthCacheVisibleSystemMessages $ Proxy @(Map SystemMessageId (Maybe UTCTime, Maybe UTCTime))
invalidateVisibleSystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => m ()
invalidateVisibleSystemMessages = memcachedByInvalidate AuthCacheVisibleSystemMessages $ Proxy @(Map SystemMessageId (Maybe UTCTime, Maybe UTCTime))
systemMessageVolatileClusterSettingsForm :: Maybe SystemMessageVolatileClusterSettings -> AForm Handler SystemMessageVolatileClusterSettings
systemMessageVolatileClusterSettingsForm (fmap Set.toList -> mPrev) = wFormToAForm $ do
currentRoute <- fromMaybe (error "systemMessageVolatileClusterSettingsForm called from 404-handler") <$> getCurrentRoute
let
volatileClusterSettingForm :: (Text -> Text) -> Maybe (VolatileClusterSettingsKey, Value) -> Form (VolatileClusterSettingsKey, Value)
volatileClusterSettingForm nudge mTemplate csrf = do
(keyRes, keyView) <- mpreq (selectField optionsFinite) ("" & addName (nudge "key" )) (view _1 <$> mTemplate)
(valRes, valView) <- mpreq (jsonField JsonFieldNormal) ("" & addName (nudge "value")) (view _2 <$> mTemplate)
return ((,) <$> keyRes <*> valRes, $(widgetFile "widgets/massinput/systemMessage/volatileClusterSettings/form"))
miAdd nudge submitView csrf = do
(formRes, formView) <- volatileClusterSettingForm nudge Nothing csrf
MsgRenderer mr <- getMsgRenderer
let res = formRes <&> \newDat@(newKey, _) oldDat -> if
| any (\(oldKey, _) -> newKey == oldKey) oldDat -> FormFailure [mr MsgSystemMessageOnVolatileClusterSettingKeyExists]
| otherwise -> FormSuccess $ pure newDat
return (res, $(widgetFile "widgets/massinput/systemMessage/volatileClusterSettings/add"))
miEdit nudge = volatileClusterSettingForm nudge . Just
miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
miButtonAction frag = Just . SomeRoute $ currentRoute :#: frag
miLayout :: MassInputLayout ListLength (VolatileClusterSettingsKey, Value) (VolatileClusterSettingsKey, Value)
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/systemMessage/volatileClusterSettings/layout")
fmap Set.fromList <$> massInputAccumEditW miAdd miEdit miButtonAction miLayout ("system-message-volatile-cluster-settings" :: Text) (fslI MsgSystemMessageOnVolatileClusterSettings) False mPrev
getMessageR, postMessageR :: CryptoUUIDSystemMessage -> Handler Html
getMessageR = postMessageR
@ -44,6 +68,7 @@ postMessageR cID = do
$ SystemMessage
<$> aopt utcTimeField (fslI MsgSystemMessageFrom) (Just systemMessageFrom)
<*> aopt utcTimeField (fslI MsgSystemMessageTo) (Just systemMessageTo)
<*> systemMessageVolatileClusterSettingsForm (Just systemMessageOnVolatileClusterSettings)
<*> apopt checkBoxField (fslI MsgSystemMessageNewsOnly) (Just systemMessageNewsOnly)
<*> apopt checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) (Just systemMessageAuthenticatedOnly)
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) (Just systemMessageSeverity)
@ -178,6 +203,7 @@ postMessageListR = do
, sortable Nothing (i18nCell MsgSystemMessageId) $ \DBRow{ dbrOutput = (Entity smId _, _) } -> anchorCellM' (encrypt smId) MessageR ciphertext
, sortable (Just "from") (i18nCell MsgSystemMessageFrom) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageFrom
, sortable (Just "to") (i18nCell MsgSystemMessageTo) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell $ maybe mempty (formatTimeW SelFormatDateTime) systemMessageTo
, sortable (Just "on-volatile-cluster-settings") (i18nCell MsgSystemMessageOnVolatileClusterSettings) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> cell . toWidget . unlines . fmap (\(k,v) -> Text.intercalate " = " [tshow k, tshow v]) $ Set.toList systemMessageOnVolatileClusterSettings
, sortable (Just "news-only") (i18nCell MsgSystemMessageNewsOnly) $ \DBRow { dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageNewsOnly
, sortable (Just "authenticated") (i18nCell MsgSystemMessageAuthenticatedOnly) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> tickmarkCell systemMessageAuthenticatedOnly
, sortable (Just "severity") (i18nCell MsgSystemMessageSeverity) $ \DBRow{ dbrOutput = (Entity _ SystemMessage{..}, _) } -> i18nCell systemMessageSeverity
@ -206,6 +232,9 @@ postMessageListR = do
, ( "to"
, SortColumn $ \systemMessage -> systemMessage E.^. SystemMessageTo
)
, ( "on-volatile-cluster-settings"
, SortColumn $ \systemMessage -> systemMessage E.^. SystemMessageOnVolatileClusterSettings
)
, ( "news-only"
, SortColumn $ \systemMessage -> systemMessage E.^. SystemMessageNewsOnly
)
@ -290,6 +319,7 @@ postMessageListR = do
((addRes, addView), addEncoding) <- runFormPost . identifyForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage
<$> aopt utcTimeField (fslI MsgSystemMessageFrom) (Just Nothing)
<*> aopt utcTimeField (fslI MsgSystemMessageTo) (Just Nothing)
<*> systemMessageVolatileClusterSettingsForm Nothing
<*> apopt checkBoxField (fslI MsgSystemMessageNewsOnly) (Just False)
<*> apopt checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) (Just False)
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) (Just Info)

View File

@ -10,8 +10,6 @@ import Handler.Utils.Communication
import qualified Database.Esqueleto.Legacy as E
import qualified Database.Esqueleto.Utils as E
import qualified Data.Map as Map
getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
getTCommR = postTCommR
@ -36,21 +34,13 @@ postTCommR tid ssh csh tutn = do
, crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR
, crJobs = crJobsCourseCommunication cid
, crTestJobs = crTestJobsCourseCommunication cid
, crRecipients = Map.fromList $
, crRecipients =
[ ( RGCourseLecturers
, E.from $ \(user `E.InnerJoin` lecturer) -> do
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
return user
)
, ( RGCourseCorrectors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
E.&&. corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
return user
)
, ( RGCourseTutors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(tutorial `E.InnerJoin` tutor) -> do
@ -59,6 +49,14 @@ postTCommR tid ssh csh tutn = do
E.&&. tutor E.^. TutorUser E.==. user E.^. UserId
return user
)
, ( RGCourseCorrectors
, E.from $ \user -> do
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
E.&&. corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
return user
)
] ++ usertuts
, crRecipientAuth = Just $ \uid -> do
isTutorialUser <- E.selectExists . E.from $ \tutorialUser ->

View File

@ -566,14 +566,14 @@ postUserPasswordR cID = do
, requireCurrent
-> wreq
(checkMap (bool (Left MsgCurrentPasswordInvalid) (Right ()) . flip (PWStore.verifyPasswordWith pwHashAlgorithm (2^)) pwHash . encodeUtf8) (const "") passwordField)
(fslI MsgCurrentPassword)
(fslI MsgCurrentPassword & addAttr "autocomplete" "current-password")
Nothing
| otherwise
-> return $ FormSuccess ()
newResult <- do
resA <- wreq passwordField (fslI MsgNewPassword) Nothing
wreq (checkBool ((== resA) . FormSuccess) MsgPasswordRepeatInvalid passwordField) (fslI MsgNewPasswordRepeat) Nothing
resA <- wreq passwordField (fslI MsgNewPassword & addAttr "autocomplete" "new-password") Nothing
wreq (checkBool ((== resA) . FormSuccess) MsgPasswordRepeatInvalid passwordField) (fslI MsgNewPasswordRepeat & addAttr "autocomplete" "new-password") Nothing
return . fmap encodeUtf8 $ currentResult *> newResult

View File

@ -75,6 +75,8 @@ postAdminUserAddR = do
, userDownloadFiles = userDefaultDownloadFiles
, userWarningDays = userDefaultWarningDays
, userShowSex = userDefaultShowSex
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
, userNotificationSettings = def
, userLanguages = Nothing
, userCsvOptions = def

View File

@ -22,7 +22,7 @@ import qualified Data.Set as Set
import qualified Data.Conduit.Combinators as C
data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors | RGCourseUnacceptedApplicants
data RecipientGroup = RGCourseParticipants | RGCourseLecturers | RGCourseCorrectors | RGCourseTutors | RGCourseParticipantsInTutorial | RGCourseUnacceptedApplicants
| RGTutorialParticipants CryptoUUIDTutorial
| RGExamRegistered CryptoUUIDExam
| RGSheetSubmittor CryptoUUIDSheet
@ -69,7 +69,7 @@ instance Button UniWorX CommunicationButton where
data CommunicationRoute = CommunicationRoute
{ crRecipients :: Map RecipientGroup (E.SqlQuery (E.SqlExpr (Entity User)))
{ crRecipients :: [(RecipientGroup, E.SqlQuery (E.SqlExpr (Entity User)))]
, crRecipientAuth :: Maybe (UserId -> DB AuthResult) -- ^ Only resolve userids given as GET-Parameter if they fulfil this criterion
, crJobs, crTestJobs :: Communication -> ConduitT () Job (YesodDB UniWorX) ()
, crHeading :: SomeMessage UniWorX
@ -111,7 +111,8 @@ commR CommunicationRoute{..} = do
mbCurrentRoute <- getCurrentRoute
(suggestedRecipients, chosenRecipients) <- runDB $ do
suggested <- for crRecipients $ \user -> E.select user
suggestedUsers <- for crRecipients $ \(_,user) -> E.select user
let suggested = zip (view _1 <$> crRecipients) suggestedUsers
let
decrypt' :: CryptoUUIDUser -> DB (Maybe (Entity User))
@ -127,21 +128,21 @@ commR CommunicationRoute{..} = do
let
lookupUser :: UserId -> User
lookupUser lId
= entityVal . unsafeHead . filter ((== lId) . entityKey) $ concat (Map.elems suggestedRecipients) ++ chosenRecipients
= entityVal . unsafeHead . filter ((== lId) . entityKey) $ concat (view _2 <$> suggestedRecipients) ++ chosenRecipients
let chosenRecipients' = Map.fromList $
[ ( (BoundedPosition $ RecipientGroup g, pos)
, (Right recp, recp `elem` map entityKey chosenRecipients)
)
| (g, recps) <- Map.toList suggestedRecipients
| (g, recps) <- suggestedRecipients
, (pos, recp) <- zip [0..] $ map entityKey recps
] ++
[ ( (BoundedPosition RecipientCustom, pos)
, (Right recp, True)
)
| (pos, recp) <- zip [0..] . Set.toList $ Set.fromList (map entityKey chosenRecipients) \\ Set.fromList (concatMap (map entityKey) $ Map.elems suggestedRecipients)
| (pos, recp) <- zip [0..] . Set.toList $ Set.fromList (map entityKey chosenRecipients) \\ Set.fromList (concatMap (map entityKey) $ view _2 <$> suggestedRecipients)
]
activeCategories = map RecipientGroup (Map.keys suggestedRecipients) `snoc` RecipientCustom
activeCategories = map RecipientGroup (view _1 <$> suggestedRecipients) `snoc` RecipientCustom
let recipientAForm :: AForm Handler (Set (Either UserEmail UserId))
recipientAForm = postProcess <$> massInputA MassInput{..} (fslI MsgCommRecipients & setTooltip MsgCommRecipientsTip) True (Just chosenRecipients')

View File

@ -135,8 +135,9 @@ data ExternalExamUserActionData
| ExternalExamUserEditResultData ExamResultPassedGrade
| ExternalExamUserDeleteData
newtype ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
data ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
{ csvEEUserMarkSynchronised :: Bool
, csvEEUserSetLabel :: Bool
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
@ -192,12 +193,19 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
coursen = externalExamCourseName
examn = externalExamExamName
uid <- requireAuthId
Entity uid currentUser <- requireAuth
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
isExamOffice <- hasReadAccessTo $ ExamOfficeR EOExamsR
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
MsgRenderer mr <- getMsgRenderer
exampleTime <- over _utctDayTime (fromInteger . round . toRational) <$> liftIO getCurrentTime
userCsvExportLabel' <- E.select . E.from $ \examOfficeLabel -> do
E.where_ $ maybe E.false (\expLbl -> examOfficeLabel E.^. ExamOfficeLabelName E.==. E.val expLbl) (csvExportLabel $ userCsvOptions currentUser)
E.&&. examOfficeLabel E.^. ExamOfficeLabelUser E.==. E.val uid
return examOfficeLabel
let userCsvExportLabel = listToMaybe userCsvExportLabel'
let
dbtSQLQuery = runReaderT $ do
result <- view queryResult
@ -245,7 +253,7 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExternalExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
lastChange <- view $ resultResult . _entityVal . _externalExamResultLastChanged
user <- view $ resultUser . _entityVal
User{..} <- view $ resultUser . _entityVal
isSynced <- view resultIsSynced
let
hasSyncs = has folded syncs
@ -363,8 +371,17 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
EEUMGrades -> Just DBTCsvEncode
{ dbtCsvExportForm = ExternalExamUserCsvExportDataGrades
<$> apopt checkBoxField (fslI MsgExternalExamUserMarkSynchronisedCsv & setTooltip MsgExternalExamUserMarkSynchronisedCsvTip) (Just False)
<*> bool
( pure False )
( maybe
(aforced checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip MsgExamOfficeLabelSetLabelOnExportForcedTip) False)
(\expLbl -> apopt checkBoxField (fslI MsgExamOfficeLabelSetLabelOnExport & setTooltip (MsgExamOfficeLabelSetLabelOnExportTip expLbl)) (Just True))
(examOfficeLabelName . entityVal <$> userCsvExportLabel)
)
isExamOffice
, dbtCsvDoEncode = \ExternalExamUserCsvExportDataGrades{..} -> C.mapM $ \(E.Value k, row) -> do
when csvEEUserMarkSynchronised $ externalExamResultMarkSynchronised k
when csvEEUserSetLabel $ maybe (return ()) (\lbl -> void $ upsert (ExamOfficeExternalExamLabel eeId lbl) [ExamOfficeExternalExamLabelLabel =. lbl]) (entityKey <$> userCsvExportLabel)
return $ encodeCsv' row
, dbtCsvName, dbtCsvSheetName
, dbtCsvNoExportData = Nothing

View File

@ -2125,10 +2125,18 @@ csvOptionsForm :: forall m.
, HandlerSite m ~ UniWorX
)
=> Maybe CsvOptions
-> Set ExamOfficeLabelName
-> AForm m CsvOptions
csvOptionsForm mPrev = hoistAForm liftHandler $ CsvOptions
csvOptionsForm mPrev (Set.toList -> exportLabels) = hoistAForm liftHandler $ CsvOptions
<$> csvFormatOptionsForm (fslI MsgCsvFormatOptions & setTooltip MsgCsvOptionsTip) (csvFormat <$> mPrev)
<*> apopt checkBoxField (fslI MsgCsvTimestamp & setTooltip MsgCsvTimestampTip) (csvTimestamp <$> mPrev)
<*> bool (aopt (selectField $ return exportLabelOptions) (fslI MsgCsvExportLabel & setTooltip MsgCsvExportLabelTip) (csvExportLabel <$> mPrev)) (pure Nothing) (null exportLabels)
where
exportLabelOptions = mkOptionList $ exportLabels <&> \exportLabel -> Option
{ optionDisplay = exportLabel
, optionInternalValue = exportLabel
, optionExternalValue = exportLabel
}
courseSelectForm :: forall ident handler.

View File

@ -0,0 +1,66 @@
module Handler.Utils.Random
( secretBoxCSPRNGT, secretBoxCSPRNGPure
, secretBoxCSPRNG'
) where
import Import.NoModel
import qualified Crypto.MAC.KMAC as Crypto
import qualified Crypto.Saltine.Class as Saltine
import Crypto.Hash.Algorithms (SHAKE256)
import Data.ByteArray (ByteArrayAccess)
import qualified Data.ByteArray as BA
import qualified Crypto.Random as Crypto
import Crypto.Error (onCryptoFailure)
import Control.Monad.Random.Lazy (RandT, Rand, evalRandT)
secretBoxCSPRNG' :: forall m m' string ba chunk a.
( MonadSecretBox m
, MonadThrow m
, Monad m'
, ByteArrayAccess string
, ByteArrayAccess chunk
, LazySequence ba chunk
)
=> (forall b. m' b -> m b)
-> string -- ^ Customization string
-> ba -- ^ Seed
-> RandT ChaChaDRG m' a
-> m a
secretBoxCSPRNG' nat str seed act = do
sBoxKey <- secretBoxKey
let seed' = toDigest $ kmaclazy str (Saltine.encode sBoxKey) seed
where toDigest :: Crypto.KMAC (SHAKE256 320) -> ByteString
toDigest = BA.convert
csprng <- fmap Crypto.drgNewSeed . onCryptoFailure throwM return $ Crypto.seedFromBinary seed'
nat $ evalRandT act csprng
secretBoxCSPRNGT :: forall m string ba chunk a.
( MonadSecretBox m
, MonadThrow m
, ByteArrayAccess string
, ByteArrayAccess chunk
, LazySequence ba chunk
)
=> string -- ^ Customization string
-> ba -- ^ Seed
-> RandT ChaChaDRG m a
-> m a
secretBoxCSPRNGT = secretBoxCSPRNG' id
secretBoxCSPRNGPure :: forall m string ba chunk a.
( MonadSecretBox m
, MonadThrow m
, ByteArrayAccess string
, ByteArrayAccess chunk
, LazySequence ba chunk
)
=> string -- ^ Customization string
-> ba -- ^ Seed
-> Rand ChaChaDRG a
-> m a
secretBoxCSPRNGPure = secretBoxCSPRNG' generalize

View File

@ -248,6 +248,15 @@ colExamFinishedOffice resultFinished = Colonnade.singleton (fromSortable header)
sortExamFinished :: OpticSortColumn (Maybe UTCTime)
sortExamFinished queryFinished = singletonMap "exam-finished" . SortColumn $ view queryFinished
colExamLabel :: OpticColonnade (Maybe ExamOfficeLabelName)
colExamLabel resultLabel = Colonnade.singleton (fromSortable header) body
where
header = Sortable (Just "exam-label") (i18nCell MsgTableExamLabel)
body = views resultLabel $ maybe mempty i18nCell
sortExamLabel :: OpticSortColumn (Maybe ExamOfficeLabelName)
sortExamLabel queryLabel = singletonMap "exam-label" . SortColumn $ view queryLabel
---------------------
-- Exam occurences --
---------------------

View File

@ -45,10 +45,16 @@ computeUserAuthenticationDigest = hashlazy . JSON.encode
data GuessUserInfo
= GuessUserMatrikelnummer { guessUserMatrikelnummer :: UserMatriculation }
| GuessUserDisplayName { guessUserDisplayName :: UserDisplayName }
| GuessUserSurname { guessUserSurname :: UserSurname }
| GuessUserFirstName { guessUserFirstName :: UserFirstName }
= GuessUserMatrikelnummer
{ guessUserMatrikelnummer :: UserMatriculation }
| GuessUserEduPersonPrincipalName
{ guessUserEduPersonPrincipalName :: UserEduPersonPrincipalName }
| GuessUserDisplayName
{ guessUserDisplayName :: UserDisplayName }
| GuessUserSurname
{ guessUserSurname :: UserSurname }
| GuessUserFirstName
{ guessUserFirstName :: UserFirstName }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Binary GuessUserInfo
@ -93,10 +99,11 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
containsAsSet x y = E.and . map (\y' -> x `E.hasInfix` E.val y') $ asWords y
toSql user pl = bool id E.not_ (is _PLNegated pl) $ case pl ^. _plVar of
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `containsAsSet` userDisplayName'
GuessUserSurname userSurname' -> user E.^. UserSurname `containsAsSet` userSurname'
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `containsAsSet` userFirstName'
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
GuessUserEduPersonPrincipalName userEPPN' -> user E.^. UserLdapPrimaryKey E.==. E.val (Just userEPPN')
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `containsAsSet` userDisplayName'
GuessUserSurname userSurname' -> user E.^. UserSurname `containsAsSet` userSurname'
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `containsAsSet` userFirstName'
go didLdap = do
let retrieveUsers = E.select . E.from $ \user -> do

View File

@ -332,12 +332,22 @@ workflowEdgePayloadFields specs = evalRWST (forM specs $ runExceptT . renderSpec
case specField of
WorkflowPayloadFieldText{..} | Nothing <- wpftPresets -> do
prev <- extractPrev @Text
let field = case wpftType of
WorkflowPayloadTextTypeText -> textField & cfStrip
WorkflowPayloadTextTypeLarge -> textareaField & isoField _Wrapped & cfStrip
WorkflowPayloadTextTypeEmail -> emailField
WorkflowPayloadTextTypeUrl -> urlFieldText
WorkflowPayloadTextTypePassword -> passwordField
addFieldSettings = case wpftType of
WorkflowPayloadTextTypePassword -> addAttr "autofill" "new-password"
_other -> id
wSetTooltip' (fmap slI18n wpftTooltip) $
f wpftOptional
(bool (textField & cfStrip) (textareaField & isoField _Wrapped & cfStrip) wpftLarge)
field
( fsl (slI18n wpftLabel)
& maybe id (addPlaceholder . slI18n) wpftPlaceholder
& maybe id (addName . ($ "text")) mNudge
& addFieldSettings
)
(prev <|> wpftDefault)
WorkflowPayloadFieldText{..} | Just (otoList -> opts) <- wpftPresets -> do

View File

@ -227,6 +227,7 @@ import Ldap.Client.Instances as Import ()
import Data.MultiSet.Instances as Import ()
import Control.Arrow.Instances as Import ()
import Data.SemVer.Instances as Import ()
import Control.Monad.Trans.Random.Instances as Import ()
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
import Crypto.Random as Import (ChaChaDRG, Seed)

View File

@ -6,6 +6,7 @@ import Model.Types.Common as Types
import Model.Types.Course as Types
import Model.Types.DateTime as Types
import Model.Types.Exam as Types
import Model.Types.ExamOffice as Types
import Model.Types.Health as Types
import Model.Types.Mail as Types
import Model.Types.Security as Types
@ -25,3 +26,4 @@ import Model.Types.Room as Types
import Model.Types.Csv as Types
import Model.Types.Upload as Types
import Model.Types.Communication as Types
import Model.Types.SystemMessage as Types

View File

@ -13,49 +13,53 @@ import Import.NoModel
import qualified Yesod.Auth.Util.PasswordStore as PWStore
type Count = Sum Integer
type Points = Centi
type Count = Sum Integer
type Points = Centi
type Email = Text
type Email = Text
type UserTitle = Text
type UserFirstName = Text
type UserSurname = Text
type UserDisplayName = Text
type UserMatriculation = Text
type UserTitle = Text
type UserFirstName = Text
type UserSurname = Text
type UserDisplayName = Text
type UserIdent = CI Text
type UserMatriculation = Text
type UserEmail = CI Email
type StudyDegreeName = Text
type StudyDegreeShorthand = Text
type StudyDegreeKey = Int
type StudyTermsName = Text
type StudyTermsShorthand = Text
type StudyTermsKey = Int
type StudySubTermsKey = Int
type StudyDegreeName = Text
type StudyDegreeShorthand = Text
type StudyDegreeKey = Int
type StudyTermsName = Text
type StudyTermsShorthand = Text
type StudyTermsKey = Int
type StudySubTermsKey = Int
type SchoolName = CI Text
type SchoolShorthand = CI Text
type CourseName = CI Text
type CourseShorthand = CI Text
type SheetName = CI Text
type MaterialName = CI Text
type UserEmail = CI Email
type UserIdent = CI Text
type TutorialName = CI Text
type ExamName = CI Text
type ExamPartName = CI Text
type ExamOccurrenceName = CI Text
type AllocationName = CI Text
type AllocationShorthand = CI Text
type SchoolName = CI Text
type SchoolShorthand = CI Text
type SubmissionGroupName = CI Text
type CourseName = CI Text
type CourseShorthand = CI Text
type MaterialName = CI Text
type TutorialName = CI Text
type SheetName = CI Text
type SubmissionGroupName = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID
type ClusterId = UUID
type TokenId = UUID
type TermCandidateIncidence = UUID
type ExamName = CI Text
type ExamPartName = CI Text
type ExamOccurrenceName = CI Text
type SessionFileReference = Digest SHA3_256
type AllocationName = CI Text
type AllocationShorthand = CI Text
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID
type ClusterId = UUID
type TokenId = UUID
type TermCandidateIncidence = UUID
type SessionFileReference = Digest SHA3_256
type WorkflowDefinitionName = CI Text
type WorkflowInstanceName = CI Text

View File

@ -51,8 +51,9 @@ nullaryPathPiece ''Quoting $ \q -> if
data CsvOptions
= CsvOptions
{ csvFormat :: CsvFormatOptions
, csvTimestamp :: Bool
{ csvFormat :: CsvFormatOptions
, csvTimestamp :: Bool
, csvExportLabel :: Maybe Text
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (Hashable, NFData)
@ -73,8 +74,9 @@ makeLenses_ ''CsvFormatOptions
instance Default CsvOptions where
def = CsvOptions
{ csvFormat = def
, csvTimestamp = False
{ csvFormat = def
, csvTimestamp = False
, csvExportLabel = Nothing
}
instance Default CsvFormatOptions where
@ -128,14 +130,16 @@ _CsvEncodeOptions = prism' fromEncode toEncode
instance ToJSON CsvOptions where
toJSON CsvOptions{..} = JSON.object
[ "format" JSON..= csvFormat
, "timestamp" JSON..= csvTimestamp
[ "format" JSON..= csvFormat
, "timestamp" JSON..= csvTimestamp
, "export-label" JSON..= csvExportLabel
]
instance FromJSON CsvOptions where
parseJSON = JSON.withObject "CsvOptions" $ \o -> do
csvFormat <- o JSON..:? "format" JSON..!= csvFormat def
csvTimestamp <- o JSON..:? "timestamp" JSON..!= csvTimestamp def
csvFormat <- o JSON..:? "format" JSON..!= csvFormat def
csvTimestamp <- o JSON..:? "timestamp" JSON..!= csvTimestamp def
csvExportLabel <- o JSON..:? "export-label" JSON..!= csvExportLabel def
return CsvOptions{..}
data CsvFormat = FormatCsv | FormatXlsx

View File

@ -0,0 +1,8 @@
module Model.Types.ExamOffice
( ExamOfficeLabelName
) where
import Import.NoModel
type ExamOfficeLabelName = Text

View File

@ -0,0 +1,6 @@
module Model.Types.SystemMessage where
import Import.NoModel
type SystemMessageVolatileClusterSettings = Set (VolatileClusterSettingsKey, Value)

View File

@ -4,6 +4,9 @@ import Import.NoModel
import Model.Types.TH.PathPiece
type UserEduPersonPrincipalName = Text
data SystemFunction
= SystemExamOffice
| SystemFaculty

View File

@ -18,6 +18,7 @@ module Model.Types.Workflow
, WorkflowPayloadFieldReference
, WorkflowPayloadTimeCapture, WorkflowPayloadTimeCapturePrecision(..)
, WorkflowPayloadTextPreset(..)
, WorkflowPayloadTextType(..)
, WorkflowPayloadField(..)
, WorkflowScope(..)
, WorkflowScope'(..), classifyWorkflowScope
@ -263,15 +264,24 @@ data WorkflowPayloadTextPreset = WorkflowPayloadTextPreset
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (NFData)
data WorkflowPayloadTextType
= WorkflowPayloadTextTypeText
| WorkflowPayloadTextTypeLarge
| WorkflowPayloadTextTypeEmail
| WorkflowPayloadTextTypeUrl
| WorkflowPayloadTextTypePassword
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite, NFData)
-- Don't forget to update the NFData instance for every change!
data WorkflowPayloadField fileid userid (payload :: Type) where
WorkflowPayloadFieldText :: { wpftLabel :: I18nText
, wpftPlaceholder :: Maybe I18nText
, wpftTooltip :: Maybe I18nHtml
, wpftDefault :: Maybe Text
, wpftLarge :: Bool
, wpftOptional :: Bool
, wpftPresets :: Maybe (NonEmpty WorkflowPayloadTextPreset)
, wpftType :: WorkflowPayloadTextType
} -> WorkflowPayloadField fileid userid Text
WorkflowPayloadFieldNumber :: { wpfnLabel :: I18nText
, wpfnPlaceholder :: Maybe I18nText
@ -383,7 +393,7 @@ instance (Ord fileid, Ord userid, Typeable fileid, Typeable userid, Ord (FileFie
instance (NFData fileid, NFData userid, NFData (FileField fileid)) => NFData (WorkflowPayloadField fileid userid payload) where
rnf = \case
WorkflowPayloadFieldText{..} -> wpftLabel `deepseq` wpftPlaceholder `deepseq` wpftTooltip `deepseq` wpftDefault `deepseq` wpftLarge `deepseq` wpftOptional `deepseq` wpftPresets `deepseq` ()
WorkflowPayloadFieldText{..} -> wpftLabel `deepseq` wpftPlaceholder `deepseq` wpftTooltip `deepseq` wpftDefault `deepseq` wpftOptional `deepseq` wpftPresets `deepseq` wpftType `deepseq` ()
WorkflowPayloadFieldNumber{..} -> wpfnLabel `deepseq` wpfnPlaceholder `deepseq` wpfnTooltip `deepseq` wpfnDefault `deepseq` wpfnMin `deepseq` wpfnMax `deepseq` wpfnStep `deepseq` wpfnOptional `deepseq` ()
WorkflowPayloadFieldBool{..} -> wpfbLabel `deepseq` wpfbTooltip `deepseq` wpfbDefault `deepseq` wpfbOptional `deepseq` ()
WorkflowPayloadFieldDay{..} -> wpfdLabel `deepseq` wpfdTooltip `deepseq` wpfdDefault `deepseq` wpfdOptional `deepseq` wpfdMaxPast `deepseq` wpfdMaxFuture `deepseq` ()
@ -843,6 +853,11 @@ deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 1
} ''WorkflowPayloadTextPreset
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 4
, allNullaryToStringTag = True
} ''WorkflowPayloadTextType
instance (FromJSON userid, Ord userid) => FromJSON (WorkflowNodeMessage userid) where
parseJSON = genericParseJSON workflowNodeMessageAesonOptions
instance (FromJSON userid, Ord userid) => FromJSON (WorkflowEdgeMessage userid) where
@ -938,9 +953,9 @@ instance (ToJSON fileid, ToJSON userid, ToJSON (FileField fileid)) => ToJSON (Wo
, "placeholder" JSON..= wpftPlaceholder
, "tooltip" JSON..= wpftTooltip
, "default" JSON..= wpftDefault
, "large" JSON..= wpftLarge
, "optional" JSON..= wpftOptional
, "presets" JSON..= wpftPresets
, "type" JSON..= wpftType
]
toJSON WorkflowPayloadFieldNumber{..} = JSON.object $ omitNothing
[ "tag" JSON..= WPFNumber'
@ -1022,6 +1037,7 @@ instance ( FromJSON fileid, FromJSON userid
wpftLarge <- o JSON..:? "large" JSON..!= False
wpftOptional <- o JSON..: "optional"
wpftPresets <- o JSON..:? "presets"
wpftType <- o JSON..:? "type" JSON..!= bool WorkflowPayloadTextTypeText WorkflowPayloadTextTypeLarge wpftLarge
return $ WorkflowPayloadSpec WorkflowPayloadFieldText{..}
WPFNumber' -> do
wpfnLabel <- o JSON..: "label"
@ -1113,13 +1129,43 @@ deriveJSON defaultOptions
pathPieceJSON ''WorkflowScope'
deriveToJSON workflowActionAesonOptions ''WorkflowAction
newtype JsonWorkflowActionUser userid = JsonWorkflowActionUser (Maybe (Maybe userid))
instance ToJSON userid => ToJSON (JsonWorkflowActionUser userid) where
toJSON (JsonWorkflowActionUser x) = case x of
Nothing -> JSON.Null
Just Nothing -> JSON.object [ "tag" JSON..= ("unauthenticated" :: Text) ]
Just (Just x') -> toJSON x'
instance FromJSON userid => FromJSON (JsonWorkflowActionUser userid) where
parseJSON JSON.Null = pure $ JsonWorkflowActionUser Nothing
parseJSON x@(JSON.Object _)
| x == JSON.object [ "tag" JSON..= ("unauthenticated" :: Text) ]
= pure . JsonWorkflowActionUser $ Just Nothing
| otherwise
= JsonWorkflowActionUser . Just . Just <$> parseJSON x
parseJSON x = JsonWorkflowActionUser . Just . Just <$> parseJSON x
instance (ToJSON fileid, ToJSON userid) => ToJSON (WorkflowAction fileid userid) where
toJSON WorkflowAction{..} = JSON.object
[ "to" JSON..= wpTo
, "via" JSON..= wpVia
, "payload" JSON..= wpPayload
, "user" JSON..= JsonWorkflowActionUser wpUser
, "time" JSON..= wpTime
]
instance ( FromJSON fileid, FromJSON userid
, Ord fileid, Ord userid
, Typeable fileid, Typeable userid
) => FromJSON (WorkflowAction fileid userid) where
parseJSON = genericParseJSON workflowActionAesonOptions
parseJSON = JSON.withObject "WorkflowAction" $ \o -> do
wpTo <- o JSON..: "to"
wpVia <- o JSON..: "via"
wpPayload <- o JSON..: "payload"
JsonWorkflowActionUser wpUser <- o JSON..: "user"
wpTime <- o JSON..: "time"
return WorkflowAction{..}
instance (ToJSON fileid, ToJSON userid) => ToJSON (WorkflowFieldPayloadW fileid userid) where
toJSON (WorkflowFieldPayloadW (WFPText t)) = JSON.object

Some files were not shown because too many files have changed in this diff Show More