Merge branch 'master' into feat/external-apis
This commit is contained in:
commit
25cb7f047a
9
.babelrc
9
.babelrc
@ -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"]
|
||||
]
|
||||
}
|
||||
|
||||
@ -11,9 +11,10 @@
|
||||
"flatpickr": "readonly",
|
||||
"$": "readonly"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"requireConfigFile": false,
|
||||
"ecmaFeatures": {
|
||||
"legacyDecorators": true
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
126
CHANGELOG.md
126
CHANGELOG.md
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as toposort from 'toposort';
|
||||
import toposort from 'toposort';
|
||||
|
||||
const DEBUG_MODE = /localhost/.test(window.location.href) ? 1 : 0;
|
||||
|
||||
|
||||
@ -165,7 +165,7 @@ export class Alerts {
|
||||
|
||||
this._elevateAlerts();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_createAlertElement(type, content, icon = 'info-circle') {
|
||||
const alertElement = document.createElement('div');
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ const COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron';
|
||||
export class CourseTeaser {
|
||||
|
||||
_element;
|
||||
_eventManager
|
||||
_eventManager;
|
||||
|
||||
constructor(element) {
|
||||
if (!element) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ const CHECKBOX_SELECTOR = '[type="checkbox"]';
|
||||
export class CheckRange {
|
||||
_lastCheckedCell = null;
|
||||
_element;
|
||||
_tableIndices
|
||||
_tableIndices;
|
||||
_columns = new Array();
|
||||
|
||||
constructor(element) {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
BIN
messages.tar.bz2
BIN
messages.tar.bz2
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -112,3 +112,5 @@ AllocNotifyNewCourseDefault: Systemweite Einstellung
|
||||
AllocNotifyNewCourseForceOff: Nein
|
||||
AllocNotifyNewCourseForceOn: Ja
|
||||
Settings: Individuelle Benutzereinstellungen
|
||||
|
||||
FormExamOffice: Prüfungsverwaltung
|
||||
|
||||
@ -112,4 +112,6 @@ LanguageChanged: Language changed successfully
|
||||
AllocNotifyNewCourseDefault: System-wide setting
|
||||
AllocNotifyNewCourseForceOff: No
|
||||
AllocNotifyNewCourseForceOn: Yes
|
||||
Settings: Settings
|
||||
Settings: Settings
|
||||
|
||||
FormExamOffice: Exam Office
|
||||
@ -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!
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
18
models/exam-office/exam-labels.model
Normal file
18
models/exam-office/exam-labels.model
Normal 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
|
||||
@ -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, ...
|
||||
|
||||
@ -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
21591
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
128
package.json
128
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
name: uniworx
|
||||
version: 25.25.0
|
||||
version: 26.0.1
|
||||
dependencies:
|
||||
- base
|
||||
- yesod
|
||||
|
||||
1539
records.json
1539
records.json
File diff suppressed because it is too large
Load Diff
2
routes
2
routes
@ -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
|
||||
|
||||
@ -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 []
|
||||
|
||||
@ -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 []
|
||||
|
||||
@ -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 []
|
||||
|
||||
17
src/Control/Monad/Trans/Random/Instances.hs
Normal file
17
src/Control/Monad/Trans/Random/Instances.hs
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -257,6 +257,8 @@ upsertCampusUser upsertMode ldapData = do
|
||||
, userDownloadFiles = userDefaultDownloadFiles
|
||||
, userWarningDays = userDefaultWarningDays
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
, userNotificationSettings = def
|
||||
, userLanguages = Nothing
|
||||
, userCsvOptions = def
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, "")])
|
||||
|
||||
@ -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 ]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -75,6 +75,8 @@ postAdminUserAddR = do
|
||||
, userDownloadFiles = userDefaultDownloadFiles
|
||||
, userWarningDays = userDefaultWarningDays
|
||||
, userShowSex = userDefaultShowSex
|
||||
, userExamOfficeGetSynced = userDefaultExamOfficeGetSynced
|
||||
, userExamOfficeGetLabels = userDefaultExamOfficeGetLabels
|
||||
, userNotificationSettings = def
|
||||
, userLanguages = Nothing
|
||||
, userCsvOptions = def
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
66
src/Handler/Utils/Random.hs
Normal file
66
src/Handler/Utils/Random.hs
Normal 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
|
||||
@ -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 --
|
||||
---------------------
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
8
src/Model/Types/ExamOffice.hs
Normal file
8
src/Model/Types/ExamOffice.hs
Normal file
@ -0,0 +1,8 @@
|
||||
module Model.Types.ExamOffice
|
||||
( ExamOfficeLabelName
|
||||
) where
|
||||
|
||||
import Import.NoModel
|
||||
|
||||
|
||||
type ExamOfficeLabelName = Text
|
||||
6
src/Model/Types/SystemMessage.hs
Normal file
6
src/Model/Types/SystemMessage.hs
Normal file
@ -0,0 +1,6 @@
|
||||
module Model.Types.SystemMessage where
|
||||
|
||||
import Import.NoModel
|
||||
|
||||
|
||||
type SystemMessageVolatileClusterSettings = Set (VolatileClusterSettingsKey, Value)
|
||||
@ -4,6 +4,9 @@ import Import.NoModel
|
||||
import Model.Types.TH.PathPiece
|
||||
|
||||
|
||||
type UserEduPersonPrincipalName = Text
|
||||
|
||||
|
||||
data SystemFunction
|
||||
= SystemExamOffice
|
||||
| SystemFaculty
|
||||
|
||||
@ -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
Reference in New Issue
Block a user