chore: merge master
This commit is contained in:
parent
2621d36b7d
commit
38a4e6cdb7
234
.gitlab-ci.yml
Normal file
234
.gitlab-ci.yml
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
default:
|
||||||
|
image:
|
||||||
|
name: fpco/stack-build:lts-13.21
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules
|
||||||
|
- .stack
|
||||||
|
- .stack-work
|
||||||
|
|
||||||
|
variables:
|
||||||
|
STACK_ROOT: "${CI_PROJECT_DIR}/.stack"
|
||||||
|
CHROME_BIN: "/usr/bin/chromium-browser"
|
||||||
|
POSTGRES_DB: uniworx_test
|
||||||
|
POSTGRES_USER: uniworx
|
||||||
|
POSTGRES_PASSWORD: uniworx
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- setup
|
||||||
|
- frontend:build
|
||||||
|
- yesod:build
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
npm install:
|
||||||
|
stage: setup
|
||||||
|
script:
|
||||||
|
- npm install
|
||||||
|
before_script: &npm
|
||||||
|
- apt-get update -y
|
||||||
|
- npm install -g n
|
||||||
|
- n stable
|
||||||
|
- npm install -g npm
|
||||||
|
- hash -r
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
name: "${CI_JOB_NAME}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
frontend:build:
|
||||||
|
stage: frontend:build
|
||||||
|
script:
|
||||||
|
- npm run frontend:build
|
||||||
|
before_script: *npm
|
||||||
|
needs:
|
||||||
|
- npm install
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- static/bundles/
|
||||||
|
name: "${CI_JOB_NAME}"
|
||||||
|
expire_in: "1 day"
|
||||||
|
dependencies:
|
||||||
|
- npm install
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
frontend:lint:
|
||||||
|
stage: lint
|
||||||
|
script:
|
||||||
|
- npm run frontend:lint
|
||||||
|
before_script: *npm
|
||||||
|
needs:
|
||||||
|
- npm install
|
||||||
|
dependencies:
|
||||||
|
- npm install
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
yesod:build:dev:
|
||||||
|
stage: yesod:build
|
||||||
|
script:
|
||||||
|
- stack build --copy-bins --local-bin-path $(pwd)/bin --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- bin/
|
||||||
|
name: "${CI_JOB_NAME}"
|
||||||
|
expire_in: "1 week"
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME !~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
yesod:build:
|
||||||
|
stage: yesod:build
|
||||||
|
script:
|
||||||
|
- stack build --copy-bins --local-bin-path $(pwd)/bin --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- bin/
|
||||||
|
name: "${CI_JOB_NAME}"
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME =~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
frontend:test:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- npm run frontend:test
|
||||||
|
needs:
|
||||||
|
- npm install
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- npm install -g n
|
||||||
|
- n stable
|
||||||
|
- npm install -g npm
|
||||||
|
- hash -r
|
||||||
|
- apt-get install -y --no-install-recommends chromium-browser
|
||||||
|
dependencies:
|
||||||
|
- npm install
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
hlint:dev:
|
||||||
|
stage: lint
|
||||||
|
script:
|
||||||
|
- stack test --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic uniworx:test:hlint
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
- yesod:build:dev # For caching
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME !~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
yesod:test:dev:
|
||||||
|
services:
|
||||||
|
- name: postgres:10.10
|
||||||
|
alias: postgres
|
||||||
|
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- stack test --coverage --fast --flag uniworx:-library-only --flag uniworx:dev --flag uniworx:pedantic --skip hlint
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
- yesod:build:dev # For caching
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME !~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
hlint:
|
||||||
|
stage: lint
|
||||||
|
script:
|
||||||
|
- stack test --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic uniworx:test:hlint
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
- yesod:build # For caching
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME =~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
yesod:test:
|
||||||
|
services:
|
||||||
|
- name: postgres:10.10
|
||||||
|
alias: postgres
|
||||||
|
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- stack test --coverage --flag uniworx:-library-only --flag uniworx:-dev --flag uniworx:pedantic --skip hlint
|
||||||
|
needs:
|
||||||
|
- frontend:build
|
||||||
|
- yesod:build # For caching
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends locales-all
|
||||||
|
- ln -s $(which g++-7) $(dirname $(which g++-7))/g++
|
||||||
|
dependencies:
|
||||||
|
- frontend:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME =~ /^v[0-9].*/
|
||||||
|
retry: 2
|
||||||
|
|
||||||
|
deploy:uniworx3:
|
||||||
|
stage: deploy
|
||||||
|
script:
|
||||||
|
- ssh -i ~/.ssh/id root@uniworx3.ifi.lmu.de <bin/uniworx
|
||||||
|
needs:
|
||||||
|
- yesod:build
|
||||||
|
- yesod:test # For sanity
|
||||||
|
- hlint # For sanity
|
||||||
|
before_script:
|
||||||
|
- apt-get update -y
|
||||||
|
- apt-get install -y --no-install-recommends openssh-client
|
||||||
|
- install -m 0700 -d ~/.ssh
|
||||||
|
- install -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts
|
||||||
|
- install -m 0400 ${SSH_PRIVATE_KEY_UNIWORX3} ~/.ssh/id
|
||||||
|
dependencies:
|
||||||
|
- yesod:build
|
||||||
|
|
||||||
|
only:
|
||||||
|
variables:
|
||||||
|
- $CI_COMMIT_REF_NAME =~ /^v[0-9].*/
|
||||||
1478
CHANGELOG.md
1478
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
64
clean.sh
64
clean.sh
@ -4,39 +4,39 @@ set -e
|
|||||||
|
|
||||||
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
case $1 in
|
if [[ -n "${1}" ]]; then
|
||||||
"")
|
target=".stack-work-${1}"
|
||||||
exec -- stack clean
|
else
|
||||||
;;
|
target=".stack-work"
|
||||||
*)
|
fi
|
||||||
target=".stack-work-${1}"
|
shift
|
||||||
shift
|
|
||||||
|
|
||||||
if [[ ! -d "${target}" ]]; then
|
if [[ ! -d "${target}" ]]; then
|
||||||
printf "%s does not exist or is no directory\n" "${target}" >&2
|
printf "%s does not exist or is no directory\n" "${target}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ -e .stack-work-clean ]]; then
|
|
||||||
printf ".stack-work-clean exists\n" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
move-back() {
|
if [[ "${target}" != ".stack-work" ]]; then
|
||||||
if [[ -d .stack-work ]]; then
|
if [[ -e .stack-work-clean ]]; then
|
||||||
mv -v .stack-work "${target}"
|
printf ".stack-work-clean exists\n" >&2
|
||||||
else
|
exit 1
|
||||||
mkdir -v "${target}"
|
fi
|
||||||
fi
|
|
||||||
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
|
|
||||||
}
|
|
||||||
|
|
||||||
mv -v .stack-work .stack-work-clean
|
move-back() {
|
||||||
mv -v "${target}" .stack-work
|
if [[ -d .stack-work ]]; then
|
||||||
trap move-back EXIT
|
mv -v .stack-work "${target}"
|
||||||
|
else
|
||||||
|
mkdir -v "${target}"
|
||||||
|
fi
|
||||||
|
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
|
||||||
|
}
|
||||||
|
|
||||||
(
|
mv -v .stack-work .stack-work-clean
|
||||||
set -ex
|
mv -v "${target}" .stack-work
|
||||||
stack clean $@
|
trap move-back EXIT
|
||||||
)
|
fi
|
||||||
;;
|
|
||||||
esac
|
(
|
||||||
|
set -ex
|
||||||
|
stack clean $@
|
||||||
|
)
|
||||||
|
|||||||
@ -30,6 +30,8 @@ session-timeout: 7200
|
|||||||
jwt-expiration: 604800
|
jwt-expiration: 604800
|
||||||
jwt-encoding: HS256
|
jwt-encoding: HS256
|
||||||
maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728"
|
maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728"
|
||||||
|
session-files-expire: 3600
|
||||||
|
prune-unreferenced-files: 86400
|
||||||
health-check-interval:
|
health-check-interval:
|
||||||
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
|
||||||
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
|
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I18n
|
* I18n
|
||||||
*
|
*
|
||||||
@ -13,10 +15,15 @@
|
|||||||
|
|
||||||
export class I18n {
|
export class I18n {
|
||||||
|
|
||||||
translations = {};
|
_translations = {};
|
||||||
|
_datetimeLocale = undefined;
|
||||||
|
|
||||||
add(id, translation) {
|
add(id, translation) {
|
||||||
this.translations[id] = translation;
|
if (!this._translations[id]) {
|
||||||
|
this._translations[id] = translation;
|
||||||
|
} else {
|
||||||
|
throw new Error('I18N Error: Attempting to set translation multiple times for »' + id + '«!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addMany(manyTranslations) {
|
addMany(manyTranslations) {
|
||||||
@ -24,9 +31,27 @@ export class I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(id) {
|
get(id) {
|
||||||
if (!this.translations[id]) {
|
if (!this._translations[id]) {
|
||||||
throw new Error('I18N Error: Translation missing for »' + id + '«!');
|
throw new Error('I18N Error: Translation missing for »' + id + '«!');
|
||||||
}
|
}
|
||||||
return this.translations[id];
|
return this._translations[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setDatetimeLocale(locale) {
|
||||||
|
if (!this._datetimeLocale) {
|
||||||
|
moment.locale(locale);
|
||||||
|
this._datetimeLocale = locale;
|
||||||
|
} else {
|
||||||
|
throw new Error('I18N Error: Attempting to set datetime locale multiple times!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDatetimeLocale() {
|
||||||
|
if (!this._datetimeLocale) {
|
||||||
|
throw new Error('I18N Error: Attempting to access datetime locale when it has not been set!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._datetimeLocale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ describe('I18n', () => {
|
|||||||
|
|
||||||
// helper function
|
// helper function
|
||||||
function expectTranslation(id, value) {
|
function expectTranslation(id, value) {
|
||||||
expect(i18n.translations[id]).toMatch(value);
|
expect(i18n.get(id)).toMatch(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
@ -38,7 +38,7 @@ describe('I18n', () => {
|
|||||||
|
|
||||||
describe('get()', () => {
|
describe('get()', () => {
|
||||||
it('should return stored translations', () => {
|
it('should return stored translations', () => {
|
||||||
i18n.translations.id1 = 'something';
|
i18n.add('id1', 'something');
|
||||||
expect(i18n.get('id1')).toMatch('something');
|
expect(i18n.get('id1')).toMatch('something');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -48,9 +48,6 @@ const DATEPICKER_CONFIG = {
|
|||||||
timeMinutes: 0,
|
timeMinutes: 0,
|
||||||
timeSeconds: 0,
|
timeSeconds: 0,
|
||||||
|
|
||||||
// german settings
|
|
||||||
// TODO: hardcoded, get from current language / settings
|
|
||||||
locale: 'de',
|
|
||||||
weekStart: 1,
|
weekStart: 1,
|
||||||
dateFormat: FORM_DATE_FORMAT_DATE_DT,
|
dateFormat: FORM_DATE_FORMAT_DATE_DT,
|
||||||
timeFormat: FORM_DATE_FORMAT_TIME_DT,
|
timeFormat: FORM_DATE_FORMAT_TIME_DT,
|
||||||
@ -86,6 +83,7 @@ export class Datepicker {
|
|||||||
datepickerInstance;
|
datepickerInstance;
|
||||||
_element;
|
_element;
|
||||||
elementType;
|
elementType;
|
||||||
|
_locale;
|
||||||
|
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@ -96,6 +94,8 @@ export class Datepicker {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._locale = window.App.i18n.getDatetimeLocale();
|
||||||
|
|
||||||
// initialize datepickerCollections singleton if not already done
|
// initialize datepickerCollections singleton if not already done
|
||||||
if (!Datepicker.datepickerCollections) {
|
if (!Datepicker.datepickerCollections) {
|
||||||
Datepicker.datepickerCollections = new Map();
|
Datepicker.datepickerCollections = new Map();
|
||||||
@ -134,7 +134,7 @@ export class Datepicker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
|
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
|
||||||
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
|
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale });
|
||||||
|
|
||||||
// reset date to something sane
|
// reset date to something sane
|
||||||
if (parsedMomentDate)
|
if (parsedMomentDate)
|
||||||
@ -180,14 +180,27 @@ export class Datepicker {
|
|||||||
// change the selected date in the tail.datetime instance if the value of the input element is changed
|
// change the selected date in the tail.datetime instance if the value of the input element is changed
|
||||||
this._element.addEventListener('change', setDatepickerDate, { once: true });
|
this._element.addEventListener('change', setDatepickerDate, { once: true });
|
||||||
|
|
||||||
// close the instance if something other than the instance was clicked (i.e. if the target is not within the datepicker instance and if any previously clicked calendar view was replaced (is not in the window anymore) because it was clicked). YES, I KNOW
|
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
|
||||||
window.addEventListener('click', event => {
|
window.addEventListener('focusout', event => {
|
||||||
if (!this.datepickerInstance.dt.contains(event.target) && window.document.contains(event.target)) {
|
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.datepickerInstance.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// close the instance on click on any element outside of the datepicker (except the input element itself)
|
||||||
|
window.addEventListener('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.datepickerInstance.close();
|
this.datepickerInstance.close();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// close the datepicker on escape keydown events
|
// close the instance on escape keydown events
|
||||||
this._element.addEventListener('keydown', event => {
|
this._element.addEventListener('keydown', event => {
|
||||||
if (event.keyCode === KEYCODE_ESCAPE) {
|
if (event.keyCode === KEYCODE_ESCAPE) {
|
||||||
this.datepickerInstance.close();
|
this.datepickerInstance.close();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ var CHECKBOX_CLASS = 'checkbox';
|
|||||||
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
|
||||||
|
|
||||||
@Utility({
|
@Utility({
|
||||||
selector: 'input[type="checkbox"]',
|
selector: 'input[type="checkbox"]:not([uw-no-checkbox])',
|
||||||
})
|
})
|
||||||
export class Checkbox {
|
export class Checkbox {
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
|
|
||||||
/* CUSTOM CHECKBOXES */
|
/* CUSTOM CHECKBOXES */
|
||||||
/* Completely replaces legacy checkbox */
|
/* Completely replaces legacy checkbox */
|
||||||
|
.checkbox [type='checkbox'], #lang-checkbox {
|
||||||
|
position: fixed;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
[type='checkbox'] {
|
|
||||||
position: fixed;
|
|
||||||
top: -1px;
|
|
||||||
left: -1px;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
@ -150,7 +150,6 @@ textarea {
|
|||||||
padding: 4px 13px;
|
padding: 4px 13px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: var(--font-base);
|
font-family: var(--font-base);
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid #dbdbdb;
|
border: 1px solid #dbdbdb;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@ -184,8 +183,8 @@ textarea {
|
|||||||
|
|
||||||
/* OPTIONS */
|
/* OPTIONS */
|
||||||
|
|
||||||
select {
|
select[size = "1"], select:not([size]) {
|
||||||
-webkit-appearance: menulist;
|
appearance: menulist;
|
||||||
}
|
}
|
||||||
|
|
||||||
select,
|
select,
|
||||||
|
|||||||
48
frontend/src/utils/navbar/navbar.js
Normal file
48
frontend/src/utils/navbar/navbar.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Utility } from '../../core/utility';
|
||||||
|
import './navbar.scss';
|
||||||
|
|
||||||
|
|
||||||
|
export const LANGUAGE_SELECT_UTIL_SELECTOR = '[uw-language-select]';
|
||||||
|
const LANGUAGE_SELECT_INITIALIZED_CLASS = 'language-select--initialized';
|
||||||
|
|
||||||
|
|
||||||
|
@Utility({
|
||||||
|
selector: LANGUAGE_SELECT_UTIL_SELECTOR,
|
||||||
|
})
|
||||||
|
export class LanguageSelectUtil {
|
||||||
|
_element;
|
||||||
|
checkbox;
|
||||||
|
|
||||||
|
constructor(element) {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error('Language Select utility needs to be passed an element!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.classList.contains(LANGUAGE_SELECT_INITIALIZED_CLASS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._element = element;
|
||||||
|
this.checkbox = element.querySelector('#lang-checkbox');
|
||||||
|
|
||||||
|
window.addEventListener('click', event => this.close(event));
|
||||||
|
|
||||||
|
element.classList.add(LANGUAGE_SELECT_INITIALIZED_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(event) {
|
||||||
|
if (!this._element.contains(event.target) && window.document.contains(event.target)) {
|
||||||
|
this.checkbox.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const NavbarUtils = [
|
||||||
|
LanguageSelectUtil,
|
||||||
|
];
|
||||||
@ -68,14 +68,7 @@
|
|||||||
color: var(--color-lightwhite);
|
color: var(--color-lightwhite);
|
||||||
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
|
transition: height .2s cubic-bezier(0.03, 0.43, 0.58, 1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
|
||||||
color: var(--color-lightwhite);
|
|
||||||
|
|
||||||
.navbar__link-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__link-icon {
|
.navbar__link-icon {
|
||||||
@ -88,6 +81,7 @@
|
|||||||
transition: opacity .2s ease;
|
transition: opacity .2s ease;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
@ -146,7 +140,9 @@
|
|||||||
.navbar__list-item {
|
.navbar__list-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color .1s ease;
|
transition: background-color .1s ease;
|
||||||
|
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
&:not(.navbar__list-item--favorite) + .navbar__list-item {
|
&:not(.navbar__list-item--favorite) + .navbar__list-item {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
@ -160,6 +156,9 @@
|
|||||||
&:not(.navbar__list-item--favorite) + .navbar__list-item {
|
&:not(.navbar__list-item--favorite) + .navbar__list-item {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,9 +218,13 @@
|
|||||||
color: var(--color-dark);
|
color: var(--color-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper {
|
.navbar .navbar__list-item:not(.navbar__list-item--active):not(.navbar__list-item--favorite):hover .navbar__link-wrapper, #lang-checkbox:checked ~ * .navbar__link-wrapper {
|
||||||
background-color: var(--color-dark);
|
background-color: var(--color-dark);
|
||||||
color: var(--color-lightwhite);
|
color: var(--color-lightwhite);
|
||||||
|
|
||||||
|
.navbar__link-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sticky state */
|
/* sticky state */
|
||||||
@ -267,3 +270,36 @@
|
|||||||
height: var(--header-height-collapsed);
|
height: var(--header-height-collapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#lang-dropdown {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: var(--header-height);
|
||||||
|
right: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||||
|
|
||||||
|
select {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#lang-checkbox:checked ~ #lang-dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#lang-dropdown {
|
||||||
|
top: var(--header-height-collapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import { MassInput } from './mass-input/mass-input';
|
|||||||
import { Modal } from './modal/modal';
|
import { Modal } from './modal/modal';
|
||||||
import { Tooltip } from './tooltips/tooltips';
|
import { Tooltip } from './tooltips/tooltips';
|
||||||
import { CourseTeaser } from './course-teaser/course-teaser';
|
import { CourseTeaser } from './course-teaser/course-teaser';
|
||||||
|
import { NavbarUtils } from './navbar/navbar';
|
||||||
|
|
||||||
export const Utils = [
|
export const Utils = [
|
||||||
Alerts,
|
Alerts,
|
||||||
@ -25,4 +26,5 @@ export const Utils = [
|
|||||||
ShowHide,
|
ShowHide,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
CourseTeaser,
|
CourseTeaser,
|
||||||
|
...NavbarUtils,
|
||||||
];
|
];
|
||||||
|
|||||||
3
messages/button/en.msg
Normal file
3
messages/button/en.msg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
AmbiguousButtons: Multiple active submit buttons
|
||||||
|
WrongButtonValue: Submit button has wrong value
|
||||||
|
MultipleButtonValues: Submit button has multiple values
|
||||||
@ -1,5 +1,6 @@
|
|||||||
CampusIdentPlaceholder: Vorname.Nachname@campus.lmu.de
|
CampusIdentPlaceholder: Vorname.Nachname@campus.lmu.de
|
||||||
CampusIdent: Campus-Kennung
|
CampusIdent: Campus-Kennung
|
||||||
CampusPassword: Passwort
|
CampusPassword: Passwort
|
||||||
|
CampusPasswordPlaceholder: Passwort
|
||||||
CampusSubmit: Abschicken
|
CampusSubmit: Abschicken
|
||||||
CampusInvalidCredentials: Ungültige Logindaten
|
CampusInvalidCredentials: Ungültige Logindaten
|
||||||
6
messages/campus/en.msg
Normal file
6
messages/campus/en.msg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CampusIdentPlaceholder: First.Last@campus.lmu.de
|
||||||
|
CampusIdent: Campus account
|
||||||
|
CampusPassword: Password
|
||||||
|
CampusPasswordPlaceholder: Password
|
||||||
|
CampusSubmit: Send
|
||||||
|
CampusInvalidCredentials: Invalid login
|
||||||
@ -1,2 +1,3 @@
|
|||||||
DummyIdent: Nutzer-Kennung
|
DummyIdent: Identifikation
|
||||||
|
DummyIdentPlaceholder: Identifikation
|
||||||
DummyNoFormData: Keine Formulardaten empfangen
|
DummyNoFormData: Keine Formulardaten empfangen
|
||||||
3
messages/dummy/en.msg
Normal file
3
messages/dummy/en.msg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DummyIdent: Identification
|
||||||
|
DummyIdentPlaceholder: Identification
|
||||||
|
DummyNoFormData: No form data received
|
||||||
@ -1,4 +1,4 @@
|
|||||||
FilesSelected: Dateien ausgewählt
|
FilesSelected: Dateien ausgewählt
|
||||||
SelectFile: Datei auswählen
|
SelectFile: Datei auswählen
|
||||||
SelectFiles: Datei(en) auswählen
|
SelectFiles: Datei(en) auswählen
|
||||||
AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicke uns gerne eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für deine Hilfe!
|
AsyncFormFailure: Da ist etwas schief gelaufen, das tut uns Leid. Falls das erneut passiert schicken Sie uns bitte eine kurze Beschreibung dieses Ereignisses über das Hilfe-Widget rechts oben. Vielen Dank für Ihre Hilfe!
|
||||||
4
messages/frontend/en.msg
Normal file
4
messages/frontend/en.msg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FilesSelected: Files selected
|
||||||
|
SelectFile: Select file
|
||||||
|
SelectFiles: Select file(s)
|
||||||
|
AsyncFormFailure: Something went wrong, we are sorry. If this error occurs again, please let us know by clicking the Support button in the upper right corner. Thank you very much!
|
||||||
@ -1,2 +1,4 @@
|
|||||||
PWHashIdent: Identifikation
|
PWHashIdent: Identifikation
|
||||||
PWHashPassword: Passwort
|
PWHashIdentPlaceholder: Identifikation
|
||||||
|
PWHashPassword: Passwort
|
||||||
|
PWHashPasswordPlaceholder: Passwort
|
||||||
4
messages/pw-hash/en.msg
Normal file
4
messages/pw-hash/en.msg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
PWHashIdent: Identification
|
||||||
|
PWHashIdentPlaceholder: Identification
|
||||||
|
PWHashPassword: Password
|
||||||
|
PWHashPasswordPlaceholder: Password
|
||||||
@ -1,5 +1,7 @@
|
|||||||
PrintDebugForStupid name@Text: Debug message "#{name}"
|
PrintDebugForStupid name@Text: Debug message "#{name}"
|
||||||
|
|
||||||
|
Logo: Uni2work
|
||||||
|
|
||||||
BtnSubmit: Senden
|
BtnSubmit: Senden
|
||||||
BtnAbort: Abbrechen
|
BtnAbort: Abbrechen
|
||||||
BtnDelete: Löschen
|
BtnDelete: Löschen
|
||||||
@ -71,9 +73,9 @@ Term: Semester
|
|||||||
TermPlaceholder: W/S + vierstellige Jahreszahl
|
TermPlaceholder: W/S + vierstellige Jahreszahl
|
||||||
|
|
||||||
TermStartDay: Erster Tag
|
TermStartDay: Erster Tag
|
||||||
TermStartDayTooltip: Üblicherweise immer 1.April oder 1.Oktober
|
TermStartDayTooltip: Üblicherweise immer 1. April oder 1. Oktober
|
||||||
TermEndDay: Letzter Tag
|
TermEndDay: Letzter Tag
|
||||||
TermEndDayTooltip: Üblicherweise immer 30.September oder 31.März
|
TermEndDayTooltip: Üblicherweise immer 30. September oder 31. März
|
||||||
TermHolidays: Feiertage
|
TermHolidays: Feiertage
|
||||||
TermHolidayPlaceholder: Feiertag
|
TermHolidayPlaceholder: Feiertag
|
||||||
TermLectureStart: Beginn Vorlesungen
|
TermLectureStart: Beginn Vorlesungen
|
||||||
@ -98,6 +100,7 @@ CourseRegistration: Kursanmeldung
|
|||||||
CourseRegisterOpen: Anmeldung möglich
|
CourseRegisterOpen: Anmeldung möglich
|
||||||
CourseRegisterOk: Erfolgreich zum Kurs angemeldet
|
CourseRegisterOk: Erfolgreich zum Kurs angemeldet
|
||||||
CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet
|
CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet
|
||||||
|
CourseApply: Zum Kurs bewerben
|
||||||
CourseApplyOk: Erfolgreich zum Kurs beworben
|
CourseApplyOk: Erfolgreich zum Kurs beworben
|
||||||
CourseRetractApplyOk: Bewerbung zum Kurs erfolgreich zurückgezogen
|
CourseRetractApplyOk: Bewerbung zum Kurs erfolgreich zurückgezogen
|
||||||
CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
|
CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
|
||||||
@ -109,8 +112,8 @@ CourseTutorial: Tutorium
|
|||||||
CourseSecretWrong: Falsches Passwort
|
CourseSecretWrong: Falsches Passwort
|
||||||
CourseSecret: Zugangspasswort
|
CourseSecret: Zugangspasswort
|
||||||
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert.
|
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert.
|
||||||
CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester.
|
CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester und Institut.
|
||||||
CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester.
|
CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester und Institut.
|
||||||
FFSheetName: Name
|
FFSheetName: Name
|
||||||
TermCourseListHeading tid@TermId: Kursübersicht #{tid}
|
TermCourseListHeading tid@TermId: Kursübersicht #{tid}
|
||||||
TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{tid} für #{school}
|
TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{tid} für #{school}
|
||||||
@ -128,7 +131,7 @@ CourseMembersCountLimited n@Int max@Int: #{n}/#{max}
|
|||||||
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"}
|
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"}
|
||||||
CourseName: Name
|
CourseName: Name
|
||||||
CourseDescription: Beschreibung
|
CourseDescription: Beschreibung
|
||||||
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
|
CourseDescriptionTip: Beliebiges Html-Markup ist gestattet
|
||||||
CourseHomepageExternal: Externe Homepage
|
CourseHomepageExternal: Externe Homepage
|
||||||
CourseShorthand: Kürzel
|
CourseShorthand: Kürzel
|
||||||
CourseShorthandUnique: Muss nur innerhalb Institut und Semester eindeutig sein. Wird verbatim in die Url der Kursseite übernommen.
|
CourseShorthandUnique: Muss nur innerhalb Institut und Semester eindeutig sein. Wird verbatim in die Url der Kursseite übernommen.
|
||||||
@ -142,14 +145,16 @@ CourseRegisterToTip: Darf auch unbegrenzt offen bleiben
|
|||||||
CourseDeregisterUntilTip: Abmeldung ist ab "Anmeldungen von" bis zu diesem Zeitpunkt erlaubt. Die Abmeldung darf auch unbegrenzt erlaubt bleiben.
|
CourseDeregisterUntilTip: Abmeldung ist ab "Anmeldungen von" bis zu diesem Zeitpunkt erlaubt. Die Abmeldung darf auch unbegrenzt erlaubt bleiben.
|
||||||
CourseFilterSearch: Volltext-Suche
|
CourseFilterSearch: Volltext-Suche
|
||||||
CourseFilterRegistered: Registriert
|
CourseFilterRegistered: Registriert
|
||||||
CourseFilterNone: Egal
|
CourseFilterNone: —
|
||||||
|
BoolIrrelevant: —
|
||||||
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
|
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
|
||||||
CourseDeleted: Kurs gelöscht
|
CourseDeleted: Kurs gelöscht
|
||||||
CourseUserTutorials: Angemeldete Tutorien
|
CourseUserTutorials: Angemeldete Tutorien
|
||||||
CourseUserNote: Notiz
|
CourseUserNote: Notiz
|
||||||
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
|
CourseUserNoteTooltip: Nur für Verwalter dieses Kurses einsehbar
|
||||||
CourseUserNoteSaved: Notizänderungen gespeichert
|
CourseUserNoteSaved: Notizänderungen gespeichert
|
||||||
CourseUserNoteDeleted: Teilnehmernotiz gelöscht
|
CourseUserNoteDeleted: Teilnehmernotiz gelöscht
|
||||||
|
CourseUserRegister: Zum Kurs anmelden
|
||||||
CourseUserDeregister: Vom Kurs abmelden
|
CourseUserDeregister: Vom Kurs abmelden
|
||||||
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
|
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
|
||||||
CourseUserRegisterTutorial: Zu einem Tutorium anmelden
|
CourseUserRegisterTutorial: Zu einem Tutorium anmelden
|
||||||
@ -184,7 +189,7 @@ CourseApplication: Bewerbung
|
|||||||
CourseApplicationIsParticipant: Kursteilnehmer
|
CourseApplicationIsParticipant: Kursteilnehmer
|
||||||
|
|
||||||
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
|
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
|
||||||
CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden
|
CourseApplicationInvalidAction: Angegebene Aktion kann nicht durchgeführt werden
|
||||||
CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben
|
CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben
|
||||||
CourseApplicationEdited csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich angepasst
|
CourseApplicationEdited csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich angepasst
|
||||||
CourseApplicationNotEdited csh@CourseShorthand: Bewerbung zu #{csh} hat sich nicht verändert
|
CourseApplicationNotEdited csh@CourseShorthand: Bewerbung zu #{csh} hat sich nicht verändert
|
||||||
@ -271,6 +276,8 @@ SheetSubmissionMode: Abgabe-Modus
|
|||||||
SheetExercise: Aufgabenstellung
|
SheetExercise: Aufgabenstellung
|
||||||
SheetHint: Hinweis
|
SheetHint: Hinweis
|
||||||
SheetHintFrom: Hinweis ab
|
SheetHintFrom: Hinweis ab
|
||||||
|
SheetHintFromPlaceholder: Datum, sonst nur für Korrektoren
|
||||||
|
SheetSolutionFromPlaceholder: Datum, sonst nur für Korrektoren
|
||||||
SheetSolution: Lösung
|
SheetSolution: Lösung
|
||||||
SheetSolutionFrom: Lösung ab
|
SheetSolutionFrom: Lösung ab
|
||||||
SheetMarking: Hinweise für Korrektoren
|
SheetMarking: Hinweise für Korrektoren
|
||||||
@ -282,9 +289,15 @@ SheetDescription: Hinweise für Teilnehmer
|
|||||||
SheetGroup: Gruppenabgabe
|
SheetGroup: Gruppenabgabe
|
||||||
SheetVisibleFrom: Sichtbar für Teilnehmer ab
|
SheetVisibleFrom: Sichtbar für Teilnehmer ab
|
||||||
SheetVisibleFromTip: Ohne Datum nie sichtbar und keine Abgabe möglich; nur für unfertige Blätter leer lassen, deren Bewertung/Fristen sich noch ändern können
|
SheetVisibleFromTip: Ohne Datum nie sichtbar und keine Abgabe möglich; nur für unfertige Blätter leer lassen, deren Bewertung/Fristen sich noch ändern können
|
||||||
SheetActiveFrom: Beginn Abgabezeitraum
|
SheetActiveFrom: Aktiv ab/Beginn Abgabezeitraum
|
||||||
SheetActiveFromTip: Download der Aufgabenstellung erst ab diesem Datum möglich
|
SheetActiveFromParticipant: Beginn Abgabezeitraum
|
||||||
SheetActiveTo: Ende Abgabezeitraum
|
SheetActiveFromParticipantNoSubmit: Herausgabe der Aufgabestellung
|
||||||
|
SheetActiveFromTip: Download der Aufgabenstellung und Abgabe erst ab diesem Datum möglich. Ohne Datum keine Abgabe und keine Herausgabe der Aufgabenstellung
|
||||||
|
SheetActiveFromUnset: Nie
|
||||||
|
SheetActiveTo: Aktiv bis/Ende Abgabezeitraum
|
||||||
|
SheetActiveToParticipant: Ende Abgabezeitraum
|
||||||
|
SheetActiveToTip: Abgabe nur bis zu diesem Datum möglich. Ohne Datum unbeschränkte Abgabe möglich (soweit gefordert).
|
||||||
|
SheetActiveToUnset: Nie
|
||||||
SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
SheetHintFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||||
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
|
||||||
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
|
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
|
||||||
@ -315,6 +328,7 @@ SubmissionEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetNa
|
|||||||
CorrectionHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{sheetName}: Korrektur
|
CorrectionHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{tid}-#{ssh}-#{csh} #{sheetName}: Korrektur
|
||||||
SubmissionMembers: Abgebende
|
SubmissionMembers: Abgebende
|
||||||
SubmissionMember: Abgebende(r)
|
SubmissionMember: Abgebende(r)
|
||||||
|
CosubmittorTip: Einladungen per E-Mail erhalten genau jene Adressen, für die nicht gesichert werden kann, dass sie mit der dahinter stehenden Person schon einmal für diesen Kurs abgegeben haben. Wenn eine angegebene Adresse einer Person zugeordnet werden kann, mit der Sie in diesem Kurs schon einmal zusammen abgegeben haben, wird der Name der Person angezeigt und die Abgabe erfolgt sofort auch im Namen jener Person.
|
||||||
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
|
SubmissionArchive: Zip-Archiv der Abgabedatei(en)
|
||||||
SubmissionFile: Datei zur Abgabe
|
SubmissionFile: Datei zur Abgabe
|
||||||
SubmissionFiles: Abgegebene Dateien
|
SubmissionFiles: Abgegebene Dateien
|
||||||
@ -333,6 +347,8 @@ CourseCorrectionsTitle: Korrekturen für diesen Kurs
|
|||||||
CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName}
|
CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName}
|
||||||
CorrectorAssignTitle: Korrektor zuweisen
|
CorrectorAssignTitle: Korrektor zuweisen
|
||||||
|
|
||||||
|
CorrectionsGrade: Abgaben online korrigieren
|
||||||
|
|
||||||
MaterialName: Name
|
MaterialName: Name
|
||||||
MaterialType: Art
|
MaterialType: Art
|
||||||
MaterialTypePlaceholder: Folien, Code, Beispiel, ...
|
MaterialTypePlaceholder: Folien, Code, Beispiel, ...
|
||||||
@ -370,6 +386,8 @@ UnauthorizedTokenNotStarted: Ihr Authorisierungs-Token ist noch nicht gültig.
|
|||||||
UnauthorizedTokenInvalid: Ihr Authorisierungs-Token konnte nicht verarbeitet werden.
|
UnauthorizedTokenInvalid: Ihr Authorisierungs-Token konnte nicht verarbeitet werden.
|
||||||
UnauthorizedTokenInvalidRoute: Ihr Authorisierungs-Token ist auf dieser Unterseite nicht gültig.
|
UnauthorizedTokenInvalidRoute: Ihr Authorisierungs-Token ist auf dieser Unterseite nicht gültig.
|
||||||
UnauthorizedTokenInvalidAuthority: Ihr Authorisierungs-Token basiert auf den Rechten eines Nutzers, der nicht mehr existiert.
|
UnauthorizedTokenInvalidAuthority: Ihr Authorisierungs-Token basiert auf den Rechten eines Nutzers, der nicht mehr existiert.
|
||||||
|
UnauthorizedTokenInvalidAuthorityGroup: Ihr Authorisierungs-Token basiert auf den Rechten einer Gruppe von Nutzern, die nicht mehr existiert.
|
||||||
|
UnauthorizedTokenInvalidAuthorityValue: Ihr Authorisierungs-Token basiert auf Rechten, deren Spezifikation nicht interpretiert werden konnte.
|
||||||
UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausgewertet werden.
|
UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausgewertet werden.
|
||||||
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
|
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
|
||||||
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
|
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
|
||||||
@ -478,7 +496,7 @@ HomeOpenAllocations: Offene Zentralanmeldungen
|
|||||||
HomeUpcomingSheets: Anstehende Übungsblätter
|
HomeUpcomingSheets: Anstehende Übungsblätter
|
||||||
HomeUpcomingExams: Bevorstehende Prüfungen
|
HomeUpcomingExams: Bevorstehende Prüfungen
|
||||||
|
|
||||||
NumCourses num@Int64: #{num} Kurse
|
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
|
||||||
CloseAlert: Schliessen
|
CloseAlert: Schliessen
|
||||||
|
|
||||||
Name: Name
|
Name: Name
|
||||||
@ -519,15 +537,15 @@ NatField name@Text: #{name} muss eine natürliche Zahl sein!
|
|||||||
JSONFieldDecodeFailure aesonFailure@String: Konnte JSON nicht parsen: #{aesonFailure}
|
JSONFieldDecodeFailure aesonFailure@String: Konnte JSON nicht parsen: #{aesonFailure}
|
||||||
SecretJSONFieldDecryptFailure: Konnte versteckte vertrauliche Daten nicht entschlüsseln
|
SecretJSONFieldDecryptFailure: Konnte versteckte vertrauliche Daten nicht entschlüsseln
|
||||||
|
|
||||||
SubmissionsAlreadyAssigned num@Int64: #{num} Abgaben waren bereits einem Korrektor zugeteilt und wurden nicht verändert:
|
SubmissionsAlreadyAssigned num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} waren bereits einem Korrektor zugeteilt und wurden nicht verändert:
|
||||||
SubmissionsAssignUnauthorized num@Int64: #{num} Abgaben können momentan nicht einem Korrektor zugeteilt werden (z.B. weil die Abgabe noch offen ist):
|
SubmissionsAssignUnauthorized num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} können momentan nicht einem Korrektor zugeteilt werden (z.B. weil die Abgabe noch offen ist):
|
||||||
UpdatedAssignedCorrectorSingle num@Int64: #{num} Abgaben wurden dem neuen Korrektor zugeteilt.
|
UpdatedAssignedCorrectorSingle num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} wurden dem neuen Korrektor zugeteilt.
|
||||||
NoCorrector: Kein Korrektor
|
NoCorrector: Kein Korrektor
|
||||||
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} Abgaben entfernt.
|
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} #{pluralDE num "Abgabe" "Abgaben"} entfernt.
|
||||||
UpdatedAssignedCorrectorsAuto num@Int64: #{num} Abgaben wurden unter den Korrektoren aufgeteilt.
|
UpdatedAssignedCorrectorsAuto num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} wurden unter den Korrektoren aufgeteilt.
|
||||||
UpdatedSheetCorrectorsAutoAssigned n@Int: #{n} #{pluralDE n "Abgabe wurde einem Korrektor" "Abgaben wurden Korrektoren"} zugteilt.
|
UpdatedSheetCorrectorsAutoAssigned n@Int: #{n} #{pluralDE n "Abgabe wurde einem Korrektor" "Abgaben wurden Korrektoren"} zugteilt.
|
||||||
UpdatedSheetCorrectorsAutoFailed n@Int: #{n} #{pluralDE n "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden.
|
UpdatedSheetCorrectorsAutoFailed n@Int: #{n} #{pluralDE n "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden.
|
||||||
CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden:
|
CouldNotAssignCorrectorsAuto num@Int64: #{num} #{pluralDE num "Abgabe konnte" "Abgaben konnten"} nicht automatisch zugewiesen werden:
|
||||||
SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
|
SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
|
||||||
|
|
||||||
SubmissionOriginal: Original
|
SubmissionOriginal: Original
|
||||||
@ -604,20 +622,21 @@ RatingNotUnicode uexc@UnicodeException: Bewertungsdatei nicht in UTF-8 kodiert:
|
|||||||
RatingMissingSeparator: Präambel der Bewertungsdatei konnte nicht identifziert werden
|
RatingMissingSeparator: Präambel der Bewertungsdatei konnte nicht identifziert werden
|
||||||
RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe
|
RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe
|
||||||
RatingInvalid parseErr@Text: Bewertungspunktzahl konnte nicht als Zahl verstanden werden: #{parseErr}
|
RatingInvalid parseErr@Text: Bewertungspunktzahl konnte nicht als Zahl verstanden werden: #{parseErr}
|
||||||
RatingFileIsDirectory: Unerwarteter Fehler: Datei ist unerlaubterweise ein Verzeichnis
|
RatingFileIsDirectory: Bewertungsdatei ist unerlaubterweise ein Verzeichnis
|
||||||
RatingNegative: Bewertungspunkte dürfen nicht negativ sein
|
RatingNegative: Bewertungspunkte dürfen nicht negativ sein
|
||||||
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
|
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
|
||||||
RatingNotExpected: Keine Bewertungen erlaubt
|
RatingNotExpected: Keine Bewertungen erlaubt
|
||||||
RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein
|
RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein
|
||||||
RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl
|
RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl
|
||||||
|
RatingFile: Bewertungsdatei
|
||||||
|
|
||||||
SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor
|
SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor
|
||||||
SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden.
|
SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden.
|
||||||
SubmissionSinkExceptionRatingWithoutUpdate: Bewertung gefunden, es ist hier aber keine Bewertung der Abgabe möglich.
|
SubmissionSinkExceptionRatingWithoutUpdate: Bewertung gefunden, es ist hier aber keine Bewertung der Abgabe möglich.
|
||||||
SubmissionSinkExceptionForeignRating smid@CryptoFileNameSubmission: Fremde Bewertung für Abgabe #{toPathPiece smid} enthalten. Bewertungen müssen sich immer auf die gleiche Abgabe beziehen!
|
SubmissionSinkExceptionForeignRating smid@CryptoFileNameSubmission: Fremde Bewertung für Abgabe #{toPathPiece smid} enthalten. Bewertungen müssen sich immer auf die gleiche Abgabe beziehen!
|
||||||
SubmissionSinkExceptionInvalidFileTitleExtension file@FilePath: Dateiname #{show file} hat keine der für dieses Übungsblatt zulässigen Dateiendungen.
|
SubmissionSinkExceptionInvalidFileTitleExtension file@FilePath: Dateiname „#{show file}“ hat keine der für dieses Übungsblatt zulässigen Dateiendungen.
|
||||||
|
|
||||||
MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufgetreten: #{error}
|
MultiSinkException name@Text error@Text: In Abgabe „#{name}“ ist ein Fehler aufgetreten: #{error}
|
||||||
|
|
||||||
NoTableContent: Kein Tabelleninhalt
|
NoTableContent: Kein Tabelleninhalt
|
||||||
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
|
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
|
||||||
@ -627,7 +646,7 @@ AdminHeading: Administration
|
|||||||
AdminUserHeading: Benutzeradministration
|
AdminUserHeading: Benutzeradministration
|
||||||
AdminUserRightsHeading: Benutzerrechte
|
AdminUserRightsHeading: Benutzerrechte
|
||||||
AdminUserAuthHeading: Benutzer-Authentifizierung
|
AdminUserAuthHeading: Benutzer-Authentifizierung
|
||||||
AdminUserHeadingFor: Benuterprofil für
|
AdminUserHeadingFor: Benutzerprofil für
|
||||||
AdminFor: Administrator
|
AdminFor: Administrator
|
||||||
LecturerFor: Dozent
|
LecturerFor: Dozent
|
||||||
LecturersFor: Dozenten
|
LecturersFor: Dozenten
|
||||||
@ -635,7 +654,6 @@ AssistantFor: Assistent
|
|||||||
AssistantsFor: Assistenten
|
AssistantsFor: Assistenten
|
||||||
TutorsFor n@Int: #{pluralDE n "Tutor" "Tutoren"}
|
TutorsFor n@Int: #{pluralDE n "Tutor" "Tutoren"}
|
||||||
CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"}
|
CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"}
|
||||||
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
|
|
||||||
UserListTitle: Komprehensive Benutzerliste
|
UserListTitle: Komprehensive Benutzerliste
|
||||||
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
AccessRightsSaved: Berechtigungen erfolgreich verändert
|
||||||
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
|
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
|
||||||
@ -647,7 +665,7 @@ DateTimeFormat: Datums- und Uhrzeitformat
|
|||||||
DateFormat: Datumsformat
|
DateFormat: Datumsformat
|
||||||
TimeFormat: Uhrzeitformat
|
TimeFormat: Uhrzeitformat
|
||||||
DownloadFiles: Dateien automatisch herunterladen
|
DownloadFiles: Dateien automatisch herunterladen
|
||||||
DownloadFilesTip: Wenn gesetzt werden Dateien von Abgaben und Übungsblättern automatisch als Download behandelt, ansonsten ist das Verhalten browserabhängig (es können z.B. PDFs im Browser geöffnet werden).
|
DownloadFilesTip: Wenn gesetzt werden Dateien automatisch als Download behandelt, ansonsten ist das Verhalten browserabhängig (es können z.B. PDFs im Browser geöffnet werden).
|
||||||
WarningDays: Fristen-Vorschau
|
WarningDays: Fristen-Vorschau
|
||||||
WarningDaysTip: Wie viele Tage im Voraus sollen Fristen von Klausuren etc. auf Ihrer Startseite angezeigt werden?
|
WarningDaysTip: Wie viele Tage im Voraus sollen Fristen von Klausuren etc. auf Ihrer Startseite angezeigt werden?
|
||||||
NotificationSettings: Erwünschte Benachrichtigungen
|
NotificationSettings: Erwünschte Benachrichtigungen
|
||||||
@ -659,6 +677,10 @@ FormCosmetics: Oberfläche
|
|||||||
FormPersonalAppearance: Öffentliche Daten
|
FormPersonalAppearance: Öffentliche Daten
|
||||||
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
|
FormFieldRequiredTip: Gekennzeichnete Pflichtfelder sind immer auszufüllen
|
||||||
|
|
||||||
|
PersonalInfoExamAchievementsWip: Die Anzeige von Prüfungsergebnissen wird momentan an dieser Stelle leider noch nicht unterstützt.
|
||||||
|
PersonalInfoOwnTutorialsWip: Die Anzeige von Tutorien, zu denen Sie als Tutor eingetragen sind wird momentan an dieser Stelle leider noch nicht unterstützt.
|
||||||
|
PersonalInfoTutorialsWip: Die Anzeige von Tutorien, zu denen Sie angemeldet sind wird momentan an dieser Stelle leider noch nicht unterstützt.
|
||||||
|
|
||||||
ActiveAuthTags: Aktivierte Authorisierungsprädikate
|
ActiveAuthTags: Aktivierte Authorisierungsprädikate
|
||||||
|
|
||||||
InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet
|
InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet
|
||||||
@ -725,8 +747,8 @@ UploadSpecificFileRequired: Zur Abgabe erforderlich
|
|||||||
|
|
||||||
NoSubmissions: Keine Abgabe
|
NoSubmissions: Keine Abgabe
|
||||||
CorrectorSubmissions: Abgabe extern mit Pseudonym
|
CorrectorSubmissions: Abgabe extern mit Pseudonym
|
||||||
UserSubmissions: Direkte Abgabe
|
UserSubmissions: Direkte Abgabe in Uni2work
|
||||||
BothSubmissions: Abgabe direkt & extern mit Pseudonym
|
BothSubmissions: Abgabe direkt in Uni2work & extern mit Pseudonym
|
||||||
|
|
||||||
SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahren (zumeist in Papierform durch Einwurf) unter Angabe eines persönlichen Pseudonyms. Korrektoren können mithilfe des Pseudonyms später Korrekturergebnisse in Uni2work eintragen, damit Sie sie einsehen können.
|
SheetCorrectorSubmissionsTip: Abgabe erfolgt über ein Uni2work-externes Verfahren (zumeist in Papierform durch Einwurf) unter Angabe eines persönlichen Pseudonyms. Korrektoren können mithilfe des Pseudonyms später Korrekturergebnisse in Uni2work eintragen, damit Sie sie einsehen können.
|
||||||
|
|
||||||
@ -738,8 +760,10 @@ SubmissionUpdated: Abgabe erfolgreich ersetzt
|
|||||||
AdminFeaturesHeading: Studiengänge
|
AdminFeaturesHeading: Studiengänge
|
||||||
StudyTerms: Studiengänge
|
StudyTerms: Studiengänge
|
||||||
StudyTerm: Studiengang
|
StudyTerm: Studiengang
|
||||||
NoStudyTermsKnown: Nicht eingeschrieben
|
NoStudyTermsKnown: Keine Studiengänge bekannt
|
||||||
StudyFeatureInference: Studiengangschlüssel-Inferenz
|
StudyFeatureInference: Studiengangschlüssel-Inferenz
|
||||||
|
StudyFeatureInferenceNoConflicts: Keine Konflikte beobachtet
|
||||||
|
StudyFeatureInferenceConflictsHeading: Studiengangseinträge mit beobachteten Konflikten
|
||||||
StudyFeatureAge: Fachsemester
|
StudyFeatureAge: Fachsemester
|
||||||
StudyFeatureDegree: Abschluss
|
StudyFeatureDegree: Abschluss
|
||||||
FieldPrimary: Hauptfach
|
FieldPrimary: Hauptfach
|
||||||
@ -757,9 +781,9 @@ DegreeShort: Abschlusskürzel
|
|||||||
StudyTermsKey: Studiengangschlüssel
|
StudyTermsKey: Studiengangschlüssel
|
||||||
StudyTermsName: Studiengang
|
StudyTermsName: Studiengang
|
||||||
StudyTermsShort: Studiengangkürzel
|
StudyTermsShort: Studiengangkürzel
|
||||||
StudyTermsChangeSuccess: Zuordnung Abschlüsse aktualisiert
|
StudyTermsChangeSuccess: Zuordnung Studiengänge aktualisiert
|
||||||
StudyDegreeChangeSuccess: Zuordnung Studiengänge aktualisiert
|
StudyDegreeChangeSuccess: Zuordnung Abschlüsse aktualisiert
|
||||||
StudyCandidateIncidence: Anmeldevorgang
|
StudyCandidateIncidence: Synchronisation
|
||||||
AmbiguousCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt
|
AmbiguousCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt
|
||||||
RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt
|
RedundantCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Kandidat" "bekannte Kandiaten"} entfernt
|
||||||
CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
|
CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
|
||||||
@ -778,6 +802,8 @@ MailTestDateTime: Test der Datumsformattierung:
|
|||||||
|
|
||||||
German: Deutsch
|
German: Deutsch
|
||||||
GermanGermany: Deutsch (Deutschland)
|
GermanGermany: Deutsch (Deutschland)
|
||||||
|
English: Englisch
|
||||||
|
EnglishEurope: Englisch (Europa)
|
||||||
|
|
||||||
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde korrigiert
|
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde korrigiert
|
||||||
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde korrigiert.
|
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde korrigiert.
|
||||||
@ -859,9 +885,9 @@ MailSubjectExamOfficeUserInvitation displayName@Text: Berücksichtigung von Prü
|
|||||||
MailSubjectPasswordReset: Uni2work-Passwort ändern bzw. setzen
|
MailSubjectPasswordReset: Uni2work-Passwort ändern bzw. setzen
|
||||||
|
|
||||||
SheetGrading: Bewertung
|
SheetGrading: Bewertung
|
||||||
SheetGradingPoints maxPoints@Points: #{maxPoints} Punkte
|
SheetGradingPoints maxPoints@Points: #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkte"}
|
||||||
SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} Punkten
|
SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkten"}
|
||||||
SheetGradingPassBinary: Bestanden/Nicht Bestanden
|
SheetGradingPassBinary: Bestanden/Nicht Bestanden
|
||||||
SheetGradingInfo: "Bestanden nach Punkten" zählt sowohl zur maximal erreichbaren Gesamtpunktzahl also auch zur Anzahl der zu bestehenden Blätter.
|
SheetGradingInfo: "Bestanden nach Punkten" zählt sowohl zur maximal erreichbaren Gesamtpunktzahl also auch zur Anzahl der zu bestehenden Blätter.
|
||||||
|
|
||||||
SheetGradingCount': Anzahl
|
SheetGradingCount': Anzahl
|
||||||
@ -915,10 +941,10 @@ NotificationTriggerExamRegistrationSoonInactive: Ich kann mich bald nicht mehr f
|
|||||||
NotificationTriggerExamDeregistrationSoonInactive: Ich kann mich bald nicht mehr von einer Prüfung abmelden
|
NotificationTriggerExamDeregistrationSoonInactive: Ich kann mich bald nicht mehr von einer Prüfung abmelden
|
||||||
NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen
|
NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen
|
||||||
NotificationTriggerAllocationStaffRegister: Ich kann Kurse bei einer neuen Zentralanmeldung eintragen
|
NotificationTriggerAllocationStaffRegister: Ich kann Kurse bei einer neuen Zentralanmeldung eintragen
|
||||||
NotificationTriggerAllocationAllocation: Ich kann Zentralanmeldung-Bewerbungen für einen meiner Kurse bewerten
|
NotificationTriggerAllocationAllocation: Ich kann Zentralanmeldungs-Bewerbungen für einen meiner Kurse bewerten
|
||||||
NotificationTriggerAllocationRegister: Ich kann mich bei einer neuen Zentralanmeldung bewerben
|
NotificationTriggerAllocationRegister: Ich kann mich bei einer neuen Zentralanmeldung bewerben
|
||||||
NotificationTriggerAllocationOutdatedRatings: Zentralanmeldung-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden
|
NotificationTriggerAllocationOutdatedRatings: Zentralanmeldungs-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden
|
||||||
NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldung-Bewerbungen für einen meiner Kurse stehen aus
|
NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldungs-Bewerbungen für einen meiner Kurse stehen aus
|
||||||
NotificationTriggerAllocationResults: Plätze wurden für eine meiner Zentralanmeldungen verteilt
|
NotificationTriggerAllocationResults: Plätze wurden für eine meiner Zentralanmeldungen verteilt
|
||||||
NotificationTriggerExamOfficeExamResults: Ich kann neue Prüfungsergebnisse einsehen
|
NotificationTriggerExamOfficeExamResults: Ich kann neue Prüfungsergebnisse einsehen
|
||||||
NotificationTriggerExamOfficeExamResultsChanged: Prüfungsergebnisse wurden verändert
|
NotificationTriggerExamOfficeExamResultsChanged: Prüfungsergebnisse wurden verändert
|
||||||
@ -936,7 +962,7 @@ NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
|
|||||||
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
|
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
|
||||||
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
|
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
|
||||||
|
|
||||||
CorrCreate: Abgaben erstellen
|
CorrCreate: Abgaben registrieren
|
||||||
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
|
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
|
||||||
InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}"
|
InvalidPseudonym pseudonym@Text: Invalides Pseudonym "#{pseudonym}"
|
||||||
InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte.
|
InvalidPseudonymSubmissionIgnored oPseudonyms@Text iPseudonym@Text: Abgabe mit Pseudonymen „#{oPseudonyms}“ wurde ignoriert, da „#{iPseudonym}“ nicht automatisiert zu einem validen Pseudonym korrigiert werden konnte.
|
||||||
@ -956,6 +982,10 @@ SheetCreateExisting: Folgende Pseudonyme haben bereits abgegeben:
|
|||||||
CorrGrade: Korrekturen eintragen
|
CorrGrade: Korrekturen eintragen
|
||||||
|
|
||||||
UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht!
|
UserAccountDeleted name@Text: Konto für #{name} wurde gelöscht!
|
||||||
|
UserSubmissionsDeleted n@Int: #{tshow n} Abgaben wurden unwiderruflich gelöscht.
|
||||||
|
UserGroupSubmissionsKept n@Int: #{tshow n} Gruppenabgaben verbleiben in der Datenbank, aber die Zuordnung zum Benutzer wurde gelöscht. Gruppenabgaben können dadurch zu Einzelabgaben werden, die dann mit dem letzten Benutzer gelöscht werden.
|
||||||
|
UserSubmissionGroupsDeleted count@Int64: #{tshow count} benannte Abgabengruppen wurden gelöscht, da sie ohne den Nutzer leer wären.
|
||||||
|
UserAccountDeleteWarning: Achtung, dies löscht den kompletten Benutzer unwiderruflich und mit allen assoziierten Daten aus der Datenbank. Prüfungsdaten müssen jedoch langfristig gespeichert bleiben!
|
||||||
|
|
||||||
HelpTitle : Hilfe
|
HelpTitle : Hilfe
|
||||||
HelpAnswer: Antworten an
|
HelpAnswer: Antworten an
|
||||||
@ -1028,7 +1058,6 @@ EncodedSecretBoxCouldNotDecodeNonce: Konnte secretbox-nonce nicht dekodieren
|
|||||||
EncodedSecretBoxCouldNotOpenSecretBox: Konnte libsodium-secretbox nicht öffnen (Verschlüsselte Daten sind nicht authentisch)
|
EncodedSecretBoxCouldNotOpenSecretBox: Konnte libsodium-secretbox nicht öffnen (Verschlüsselte Daten sind nicht authentisch)
|
||||||
EncodedSecretBoxCouldNotDecodePlaintext aesonErr@String: Konnte Klartext nicht JSON-dekodieren: #{aesonErr}
|
EncodedSecretBoxCouldNotDecodePlaintext aesonErr@String: Konnte Klartext nicht JSON-dekodieren: #{aesonErr}
|
||||||
ErrMsgHeading: Fehlermeldung entschlüsseln
|
ErrMsgHeading: Fehlermeldung entschlüsseln
|
||||||
ErrorCryptoIdMismatch: Verschlüsselte Id der Abgabe passte nicht zu anderen Daten
|
|
||||||
|
|
||||||
InvalidRoute: Konnte URL nicht interpretieren
|
InvalidRoute: Konnte URL nicht interpretieren
|
||||||
|
|
||||||
@ -1065,6 +1094,7 @@ MenuProfileData: Persönliche Daten
|
|||||||
MenuTermCreate: Neues Semester anlegen
|
MenuTermCreate: Neues Semester anlegen
|
||||||
MenuCourseNew: Neuen Kurs anlegen
|
MenuCourseNew: Neuen Kurs anlegen
|
||||||
MenuTermEdit: Semester editieren
|
MenuTermEdit: Semester editieren
|
||||||
|
MenuTermCurrent: Aktuelles Semester
|
||||||
MenuCorrection: Korrektur
|
MenuCorrection: Korrektur
|
||||||
MenuCorrections: Korrekturen
|
MenuCorrections: Korrekturen
|
||||||
MenuCorrectionsOwn: Meine Korrekturen
|
MenuCorrectionsOwn: Meine Korrekturen
|
||||||
@ -1093,8 +1123,8 @@ MenuCorrectionsUpload: Korrekturen hochladen
|
|||||||
MenuCorrectionsDownload: Offene Abgaben herunterladen
|
MenuCorrectionsDownload: Offene Abgaben herunterladen
|
||||||
MenuCorrectionsCreate: Abgaben registrieren
|
MenuCorrectionsCreate: Abgaben registrieren
|
||||||
MenuCorrectionsGrade: Abgaben online korrigieren
|
MenuCorrectionsGrade: Abgaben online korrigieren
|
||||||
MenuCorrectionsAssign: Zuteilung Korrekturen
|
MenuCorrectionsAssign: Zuteilung der Korrekturen
|
||||||
MenuCorrectionsAssignSheet name@Text: Zuteilung Korrekturen von #{name}
|
MenuCorrectionsAssignSheet name@Text: Zuteilung der Korrekturen von #{name}
|
||||||
MenuAuthPreds: Authorisierungseinstellungen
|
MenuAuthPreds: Authorisierungseinstellungen
|
||||||
MenuTutorialDelete: Tutorium löschen
|
MenuTutorialDelete: Tutorium löschen
|
||||||
MenuTutorialEdit: Tutorium editieren
|
MenuTutorialEdit: Tutorium editieren
|
||||||
@ -1108,7 +1138,7 @@ MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
|
|||||||
MenuExamOfficeExams: Prüfungen
|
MenuExamOfficeExams: Prüfungen
|
||||||
MenuExamOfficeFields: Fächer
|
MenuExamOfficeFields: Fächer
|
||||||
MenuExamOfficeUsers: Benutzer
|
MenuExamOfficeUsers: Benutzer
|
||||||
MenuLecturerInvite: Dozenten hinzufügen
|
MenuLecturerInvite: Funktionäre hinzufügen
|
||||||
MenuAllocationInfo: Hinweise zum Ablauf einer Zentralanmeldung
|
MenuAllocationInfo: Hinweise zum Ablauf einer Zentralanmeldung
|
||||||
MenuCourseApplicationsFiles: Dateien aller Bewerbungen
|
MenuCourseApplicationsFiles: Dateien aller Bewerbungen
|
||||||
MenuSchoolList: Institute
|
MenuSchoolList: Institute
|
||||||
@ -1118,6 +1148,61 @@ MenuCourseNewsEdit: Kursnachricht bearbeiten
|
|||||||
MenuCourseEventNew: Neuer Kurstermin
|
MenuCourseEventNew: Neuer Kurstermin
|
||||||
MenuCourseEventEdit: Kurstermin bearbeiten
|
MenuCourseEventEdit: Kurstermin bearbeiten
|
||||||
|
|
||||||
|
BreadcrumbSubmissionFile: Datei
|
||||||
|
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
||||||
|
BreadcrumbCryptoIDDispatch: CryptoID-Weiterleitung
|
||||||
|
BreadcrumbCourseAppsFiles: Bewerbungsdateien
|
||||||
|
BreadcrumbCourseNotes: Kursnotizen
|
||||||
|
BreadcrumbHiWis: Korrektoren
|
||||||
|
BreadcrumbMaterial: Material
|
||||||
|
BreadcrumbSheet: Übungsblatt
|
||||||
|
BreadcrumbTutorial: Tutorium
|
||||||
|
BreadcrumbExam: Prüfung
|
||||||
|
BreadcrumbApplicant: Bewerber
|
||||||
|
BreadcrumbCourseRegister: Anmelden
|
||||||
|
BreadcrumbCourseRegisterTemplate: Bewerbungsvorlagen
|
||||||
|
BreadcrumbCourseFavourite: Favorisieren
|
||||||
|
BreadcrumbCourse: Kurs
|
||||||
|
BreadcrumbAllocationRegister: Teilnahme registrieren
|
||||||
|
BreadcrumbAllocation: Zentralanmeldung
|
||||||
|
BreadcrumbTerm: Semester
|
||||||
|
BreadcrumbSchool: Institut
|
||||||
|
BreadcrumbUser: Benutzer
|
||||||
|
BreadcrumbStatic: Statische Resource
|
||||||
|
BreadcrumbFavicon: Favicon
|
||||||
|
BreadcrumbRobots: robots.txt
|
||||||
|
BreadcrumbMetrics: Metriken
|
||||||
|
BreadcrumbLecturerInvite: Einladung zum Kursverwalter
|
||||||
|
BreadcrumbExamOfficeUserInvite: Einladung bzgl. Prüfungsleistungen
|
||||||
|
BreadcrumbFunctionaryInvite: Einladung zum Instituts-Funktionär
|
||||||
|
BreadcrumbUserDelete: Nutzer-Account löschen
|
||||||
|
BreadcrumbUserHijack: Nutzer-Sitzung übernehmen
|
||||||
|
BreadcrumbSystemMessage: Statusmeldung
|
||||||
|
BreadcrumbSubmission: Abgabe
|
||||||
|
BreadcrumbCourseNews: Kursnachricht
|
||||||
|
BreadcrumbCourseNewsDelete: Kursnachricht löschen
|
||||||
|
BreadcrumbCourseEventDelete: Kurstermin löschen
|
||||||
|
BreadcrumbProfile: Einstellungen
|
||||||
|
BreadcrumbAllocationInfo: Ablauf einer Zentralanmeldung
|
||||||
|
BreadcrumbCourseParticipantInvitation: Einladung zum Kursteilnehmer
|
||||||
|
BreadcrumbMaterialArchive: Archiv
|
||||||
|
BreadcrumbMaterialFile: Datei
|
||||||
|
BreadcrumbSheetArchive: Dateien
|
||||||
|
BreadcrumbSheetIsCorrector: Korrektor-Überprüfung
|
||||||
|
BreadcrumbSheetPseudonym: Pseudonym
|
||||||
|
BreadcrumbSheetCorrectorInvite: Einladung zum Korrektor
|
||||||
|
BreadcrumbSheetFile: Datei
|
||||||
|
BreadcrumbTutorialRegister: Anmelden
|
||||||
|
BreadcrumbTutorInvite: Einladung zum Tutor
|
||||||
|
BreadcrumbExamCorrectorInvite: Einladung zum Prüfungskorrektor
|
||||||
|
BreadcrumbExamParticipantInvite: Einladung zum Prüfungsteilnehmer
|
||||||
|
BreadcrumbExamRegister: Anmelden
|
||||||
|
BreadcrumbApplicationFiles: Bewerbungsdateien
|
||||||
|
BreadcrumbCourseNewsArchive: Archiv
|
||||||
|
BreadcrumbCourseNewsFile: Datei
|
||||||
|
|
||||||
|
TitleMetrics: Metriken
|
||||||
|
|
||||||
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
|
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
|
||||||
AuthPredsActive: Aktive Authorisierungsprädikate
|
AuthPredsActive: Aktive Authorisierungsprädikate
|
||||||
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
|
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
|
||||||
@ -1196,7 +1281,7 @@ RGTutorialParticipants: Tutorium-Teilnehmer
|
|||||||
|
|
||||||
MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg)
|
MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg)
|
||||||
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Addressen möglich
|
MultiEmailFieldTip: Es sind mehrere, Komma-separierte, E-Mail-Addressen möglich
|
||||||
EmailInvitationWarning: Dem System ist kein Nutzer mit dieser Addresse bekannt. Es wird eine Einladung per E-Mail versandt.
|
EmailInvitationWarning: Diese Adresse konnte mit Ihren aktuellen Rechten keinem Uni2work-Benutzer zugeordnet werden (ggf. unter gewissen Einschränkungen). Es wird eine Einladung per E-Mail versandt.
|
||||||
|
|
||||||
LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lType} für #{csh} eingetragen
|
LecturerInvitationAccepted lType@Text csh@CourseShorthand: Sie wurden als #{lType} für #{csh} eingetragen
|
||||||
LecturerInvitationDeclined csh@CourseShorthand: Sie haben die Einladung, Kursverwalter für #{csh} zu werden, abgelehnt
|
LecturerInvitationDeclined csh@CourseShorthand: Sie haben die Einladung, Kursverwalter für #{csh} zu werden, abgelehnt
|
||||||
@ -1289,7 +1374,7 @@ TutorialTutorControlled: Tutoren dürfen Tutorium editieren
|
|||||||
TutorialTutorControlledTip: Sollen Tutoren beliebige Aspekte dieses Tutoriums (Name, Registrierungs-Gruppe, Raum, Zeit, andere Tutoren, ...) beliebig editieren dürfen?
|
TutorialTutorControlledTip: Sollen Tutoren beliebige Aspekte dieses Tutoriums (Name, Registrierungs-Gruppe, Raum, Zeit, andere Tutoren, ...) beliebig editieren dürfen?
|
||||||
|
|
||||||
CourseExams: Prüfungen
|
CourseExams: Prüfungen
|
||||||
CourseTutorials: Übungen
|
CourseTutorials: Tutorien
|
||||||
|
|
||||||
ParticipantsN n@Int: #{n} Teilnehmer
|
ParticipantsN n@Int: #{n} Teilnehmer
|
||||||
TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen?
|
TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen?
|
||||||
@ -1412,7 +1497,7 @@ ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positi
|
|||||||
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
|
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
|
||||||
|
|
||||||
ExamAutomaticOccurrenceAssignment: Automatische Termin- bzw. Raumzuteilung
|
ExamAutomaticOccurrenceAssignment: Automatische Termin- bzw. Raumzuteilung
|
||||||
ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer zum Zeitpunkt der Bekanntgabe der Raum- bzw. Terminzuteilung automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich.
|
ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich.
|
||||||
ExamOccurrenceRule: Verfahren
|
ExamOccurrenceRule: Verfahren
|
||||||
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
|
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
|
||||||
ExamRoomManual': Keine automatische Zuteilung
|
ExamRoomManual': Keine automatische Zuteilung
|
||||||
@ -1457,7 +1542,7 @@ ExamPartName: Titel
|
|||||||
ExamPartNameTip: Wird den Studierenden angezeigt
|
ExamPartNameTip: Wird den Studierenden angezeigt
|
||||||
ExamPartMaxPoints: Maximalpunktzahl
|
ExamPartMaxPoints: Maximalpunktzahl
|
||||||
ExamPartWeight: Gewichtung
|
ExamPartWeight: Gewichtung
|
||||||
ExamPartWeightTip: Wird vor Anzeige oder Notenberechnung mit der erreichten Punktzahl und der Maximalpunktzahl multipliziert; Änderungen hier passen auch bestehende Korrekturergebnisse an
|
ExamPartWeightTip: Wird vor Anzeige oder automatischen Notenberechnung mit der erreichten Punktzahl und der Maximalpunktzahl multipliziert; Änderungen hier passen also auch bestehende Korrekturergebnisse an (derart geänderte Noten müssen erneut manuell übernommen werden)
|
||||||
ExamPartResultPoints: Erreichte Punkte
|
ExamPartResultPoints: Erreichte Punkte
|
||||||
|
|
||||||
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
|
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
|
||||||
@ -1527,6 +1612,8 @@ ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfun
|
|||||||
ExamOfficeExamUsersHeading: Prüfungsleistungen
|
ExamOfficeExamUsersHeading: Prüfungsleistungen
|
||||||
|
|
||||||
CsvFile: CSV-Datei
|
CsvFile: CSV-Datei
|
||||||
|
CsvImport: CSV-Import
|
||||||
|
CsvExport: CSV-Export
|
||||||
CsvModifyExisting: Existierende Einträge angleichen
|
CsvModifyExisting: Existierende Einträge angleichen
|
||||||
CsvAddNew: Neue Einträge einfügen
|
CsvAddNew: Neue Einträge einfügen
|
||||||
CsvDeleteMissing: Fehlende Einträge entfernen
|
CsvDeleteMissing: Fehlende Einträge entfernen
|
||||||
@ -1681,6 +1768,7 @@ SchoolFunctionInvitationAccepted school@SchoolName renderedFunction@Text: #{rend
|
|||||||
AllocationActive: Aktiv
|
AllocationActive: Aktiv
|
||||||
AllocationName: Name
|
AllocationName: Name
|
||||||
AllocationAvailableCourses: Kurse
|
AllocationAvailableCourses: Kurse
|
||||||
|
AllocationApplication: Bewerbung
|
||||||
AllocationAppliedCourses: Bewerbungen
|
AllocationAppliedCourses: Bewerbungen
|
||||||
AllocationNumCoursesAvailableApplied available@Int applied@Int: Sie haben sich bisher für #{applied}/#{available} #{pluralDE applied "Kurs" "Kursen"} beworben
|
AllocationNumCoursesAvailableApplied available@Int applied@Int: Sie haben sich bisher für #{applied}/#{available} #{pluralDE applied "Kurs" "Kursen"} beworben
|
||||||
AllocationTitle termText@Text ssh'@SchoolShorthand allocation@AllocationName: #{termText} - #{ssh'}: #{allocation}
|
AllocationTitle termText@Text ssh'@SchoolShorthand allocation@AllocationName: #{termText} - #{ssh'}: #{allocation}
|
||||||
@ -1879,7 +1967,7 @@ AcceptApplicationsSecondaryRandom: Zufällig
|
|||||||
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
|
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
|
||||||
|
|
||||||
CsvOptions: CSV-Optionen
|
CsvOptions: CSV-Optionen
|
||||||
CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt. Als Zeichenkodierung wird beim Import stets Unicode erwartet.
|
CsvOptionsTip: Diese Einstellungen betreffen primär den CSV-Export; beim Import werden die meisten Einstellungen automatisch ermittelt. Als Zeichenkodierung wird beim Import die selbe Kodierung wie beim Export erwartet.
|
||||||
CsvFormatOptions: Dateiformat
|
CsvFormatOptions: Dateiformat
|
||||||
CsvTimestamp: Zeitstempel
|
CsvTimestamp: Zeitstempel
|
||||||
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
|
||||||
@ -1898,6 +1986,7 @@ CsvDelimiterNull: Null-Byte
|
|||||||
CsvDelimiterTab: Tabulator
|
CsvDelimiterTab: Tabulator
|
||||||
CsvDelimiterComma: Komma
|
CsvDelimiterComma: Komma
|
||||||
CsvDelimiterColon: Doppelpunkt
|
CsvDelimiterColon: Doppelpunkt
|
||||||
|
CsvDelimiterSemicolon: Strichpunkt
|
||||||
CsvDelimiterBar: Senkrechter Strich
|
CsvDelimiterBar: Senkrechter Strich
|
||||||
CsvDelimiterSpace: Leerzeichen
|
CsvDelimiterSpace: Leerzeichen
|
||||||
CsvDelimiterUnitSep: Teilgruppentrennzeichen
|
CsvDelimiterUnitSep: Teilgruppentrennzeichen
|
||||||
@ -1989,5 +2078,67 @@ ShowSex: Geschlechter anderer Nutzer anzeigen
|
|||||||
ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden?
|
ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden?
|
||||||
|
|
||||||
StudySubTermsParentKey: Elter
|
StudySubTermsParentKey: Elter
|
||||||
StudyTermsDefaultDegree: Abschluss
|
StudyTermsDefaultDegree: Default Abschluss
|
||||||
StudyTermsDefaultFieldType: Typ
|
StudyTermsDefaultFieldType: Default Typ
|
||||||
|
|
||||||
|
MenuLanguage: Sprache
|
||||||
|
LanguageChanged: Sprache erfolgreich geändert
|
||||||
|
|
||||||
|
ProfileCorrector: Korrektor
|
||||||
|
ProfileCourses: Eigene Kurse
|
||||||
|
ProfileCourseParticipations: Kursanmeldungen
|
||||||
|
ProfileCourseExamResults: Prüfungsleistungen
|
||||||
|
ProfileTutorials: Eigene Tutorien
|
||||||
|
ProfileTutorialParticipations: Tutorien
|
||||||
|
ProfileSubmissionGroups: Abgabegruppen
|
||||||
|
ProfileSubmissions: Abgaben
|
||||||
|
ProfileRemark: Hinweis
|
||||||
|
ProfileGroupSubmissionDates: Bei Gruppenabgaben wird kein Datum angezeigt, wenn Sie die Gruppenabgabe nie selbst hochgeladen haben.
|
||||||
|
ProfileCorrectorRemark: Die oberhalb angezeigte Tabelle zeigt nur prinzipielle Einteilungen als Korrektor zu einem Übungsblatt. Auch ohne Einteilung können Korrekturen einzeln zugewiesen werden, welche hier dann nicht aufgeführt werden.
|
||||||
|
ProfileCorrections: Auflistung aller zugewiesenen Korrekturen
|
||||||
|
|
||||||
|
GroupSizeNotNatural: „Gruppengröße“ muss eine natürliche Zahl sein
|
||||||
|
AmbiguousEmail: E-Mail Adresse nicht eindeutig
|
||||||
|
CourseDescriptionPlaceholder: Bitte mindestens die Modulbeschreibung angeben
|
||||||
|
CourseHomepageExternalPlaceholder: Optionale externe URL
|
||||||
|
PointsPlaceholder: Punktezahl
|
||||||
|
RFC1766: RFC1766-Sprachcode
|
||||||
|
|
||||||
|
TermShort: Kürzel
|
||||||
|
TermCourseCount: Kurse
|
||||||
|
TermStart: Semesteranfang
|
||||||
|
TermEnd: Semesterende
|
||||||
|
TermStartMustMatchName: Jahreszahl im Namenskürzel stimmt nicht mit Semesterbeginn überein.
|
||||||
|
TermEndMustBeAfterStart: Semester darf nicht enden, bevor es beginnt.
|
||||||
|
TermLectureEndMustBeAfterStart: Vorlesungszeit muss vor ihrem Ende anfgangen.
|
||||||
|
TermStartMustBeBeforeLectureStart: Semester muss vor der Vorlesungszeit beginnen.
|
||||||
|
TermEndMustBeAfterLectureEnd: Vorlesungszeit muss vor dem Semester enden.
|
||||||
|
AdminPageEmpty: Diese Seite soll eine Übersichtsseite für Administratoren werden. Aktuell finden sich hier nur Links zu wichtigen Administrator-Funktionalitäten.
|
||||||
|
HaveCorrectorAccess sheetName@SheetName: Sie haben Korrektor-Zugang zu #{original sheetName}.
|
||||||
|
FavouritesPlaceholder: Anzahl Favoriten
|
||||||
|
FavouritesNotNatural: Anzahl der Favoriten muss eine natürliche Zahl sein!
|
||||||
|
FavouritesSemestersPlaceholder: Anzahl Semester
|
||||||
|
FavouritesSemestersNotNatural: Anzahl der Favoriten-Semester muss eine natürliche Zahl sein!
|
||||||
|
|
||||||
|
ProfileTitle: Benutzereinstellungen
|
||||||
|
|
||||||
|
GlossaryTitle: Begriffsverzeichnis
|
||||||
|
MenuGlossary: Begriffsverzeichnis
|
||||||
|
|
||||||
|
Applicant: Bewerber
|
||||||
|
CourseParticipant: Kursteilnehmer
|
||||||
|
Administrator: Administrator
|
||||||
|
CsvFormat: CSV-Format
|
||||||
|
ExerciseSheet: Übungsblatt
|
||||||
|
DefinitionCourseEvents: Kurstermine
|
||||||
|
DefinitionCourseNews: Kurs-Aktuelles
|
||||||
|
Invitations: Einladungen
|
||||||
|
SheetSubmission: Abgabe
|
||||||
|
CommCourse: Kursmitteilung
|
||||||
|
CommTutorial: Tutorium-Mitteilung
|
||||||
|
Clone: Klonen
|
||||||
|
Deficit: Defizit
|
||||||
|
|
||||||
|
MetricNoSamples: Keine Messwerte
|
||||||
|
MetricName: Name
|
||||||
|
MetricValue: Wert
|
||||||
2139
messages/uniworx/en-eu.msg
Normal file
2139
messages/uniworx/en-eu.msg
Normal file
File diff suppressed because it is too large
Load Diff
@ -6,3 +6,9 @@ File
|
|||||||
content ByteString Maybe -- Nothing iff this is a directory
|
content ByteString Maybe -- Nothing iff this is a directory
|
||||||
modified UTCTime
|
modified UTCTime
|
||||||
deriving Show Eq Generic
|
deriving Show Eq Generic
|
||||||
|
|
||||||
|
SessionFile
|
||||||
|
user UserId
|
||||||
|
reference SessionFileReference
|
||||||
|
file FileId
|
||||||
|
touched UTCTime
|
||||||
@ -16,3 +16,10 @@ CronLastExec
|
|||||||
time UTCTime -- When was the job executed
|
time UTCTime -- When was the job executed
|
||||||
instance InstanceId -- Which uni2work-instance did the work
|
instance InstanceId -- Which uni2work-instance did the work
|
||||||
UniqueCronLastExec job
|
UniqueCronLastExec job
|
||||||
|
|
||||||
|
|
||||||
|
SentNotification
|
||||||
|
content Value
|
||||||
|
user UserId
|
||||||
|
time UTCTime
|
||||||
|
instance InstanceId
|
||||||
@ -6,8 +6,8 @@ Sheet -- exercise sheet for a given course
|
|||||||
grouping SheetGroup -- May participants submit in groups of certain sizes?
|
grouping SheetGroup -- May participants submit in groups of certain sizes?
|
||||||
markingText Html Maybe -- Instructons for correctors, included in marking templates
|
markingText Html Maybe -- Instructons for correctors, included in marking templates
|
||||||
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
|
||||||
activeFrom UTCTime -- Download of questions and submission is permitted afterwards
|
activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards
|
||||||
activeTo UTCTime -- Submission is only permitted before
|
activeTo UTCTime Maybe -- Submission is only permitted before
|
||||||
hintFrom UTCTime Maybe -- Additional files are made available
|
hintFrom UTCTime Maybe -- Additional files are made available
|
||||||
solutionFrom UTCTime Maybe -- Solution is made available
|
solutionFrom UTCTime Maybe -- Solution is made available
|
||||||
submissionMode SubmissionMode -- Submission upload by students and/or through tutors?
|
submissionMode SubmissionMode -- Submission upload by students and/or through tutors?
|
||||||
|
|||||||
@ -28,7 +28,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-only display format for user; user-defined
|
dateFormat DateTimeFormat "default='%d.%m.%Y'" -- preferred Date-only display format for user; user-defined
|
||||||
timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined
|
timeFormat DateTimeFormat "default='%R'" -- preferred Time-only display format for user; user-defined
|
||||||
downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this)
|
downloadFiles Bool default=false -- Should files be opened in browser or downloaded? (users often oblivious that their browser has a setting for this)
|
||||||
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined
|
languages Languages Maybe -- Preferred language; user-defined
|
||||||
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
|
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
|
||||||
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
|
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
|
||||||
csvOptions CsvOptions "default='{}'::jsonb"
|
csvOptions CsvOptions "default='{}'::jsonb"
|
||||||
@ -102,4 +102,13 @@ StudySubTermParentCandidate
|
|||||||
StudyTermStandaloneCandidate
|
StudyTermStandaloneCandidate
|
||||||
incidence TermCandidateIncidence
|
incidence TermCandidateIncidence
|
||||||
key Int
|
key Int
|
||||||
deriving Show Eq Ord
|
deriving Show Eq Ord
|
||||||
|
|
||||||
|
UserGroupMember
|
||||||
|
group UserGroupName
|
||||||
|
user UserId
|
||||||
|
primary Checkmark nullable
|
||||||
|
|
||||||
|
UniquePrimaryUserGroupMember group primary !force
|
||||||
|
UniqueUserGroupMember group user
|
||||||
|
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "7.19.2",
|
"version": "7.25.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "7.19.2",
|
"version": "7.25.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -42,10 +42,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postbump": "./sync-versions.hs && git add -- package.yaml"
|
"postbump": "./sync-versions.hs && git add -- package.yaml"
|
||||||
},
|
},
|
||||||
"commitUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/{{hash}}",
|
"commitUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/{{hash}}",
|
||||||
"compareUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/{{previousTag}}...{{currentTag}}",
|
"compareUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/{{previousTag}}...{{currentTag}}",
|
||||||
"issueUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/{{id}}",
|
"issueUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/{{id}}",
|
||||||
"userUrlFormat": "https://gitlab.cip.ifi.lmu.de/{{user}}"
|
"userUrlFormat": "https://gitlab2.rz.ifi.lmu.de/{{user}}"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
|
|||||||
11
package.yaml
11
package.yaml
@ -1,5 +1,5 @@
|
|||||||
name: uniworx
|
name: uniworx
|
||||||
version: 7.19.2
|
version: 7.25.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- base >=4.9.1.0 && <5
|
- base >=4.9.1.0 && <5
|
||||||
@ -140,6 +140,10 @@ dependencies:
|
|||||||
- retry
|
- retry
|
||||||
- generic-lens
|
- generic-lens
|
||||||
- array
|
- array
|
||||||
|
- cookie
|
||||||
|
- prometheus-client
|
||||||
|
- prometheus-metrics-ghc
|
||||||
|
- wai-middleware-prometheus
|
||||||
|
|
||||||
other-extensions:
|
other-extensions:
|
||||||
- GeneralizedNewtypeDeriving
|
- GeneralizedNewtypeDeriving
|
||||||
@ -233,10 +237,7 @@ executables:
|
|||||||
uniworx:
|
uniworx:
|
||||||
main: main.hs
|
main: main.hs
|
||||||
source-dirs: app
|
source-dirs: app
|
||||||
ghc-options:
|
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -T"
|
||||||
- -threaded
|
|
||||||
- -rtsopts
|
|
||||||
- -with-rtsopts=-N
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- uniworx
|
- uniworx
|
||||||
when:
|
when:
|
||||||
|
|||||||
6
routes
6
routes
@ -40,7 +40,8 @@
|
|||||||
/auth AuthR Auth getAuth !free
|
/auth AuthR Auth getAuth !free
|
||||||
|
|
||||||
/favicon.ico FaviconR GET !free
|
/favicon.ico FaviconR GET !free
|
||||||
/robots.txt RobotsR GET !free
|
/robots.txt RobotsR GET !free
|
||||||
|
/metrics MetricsR GET
|
||||||
|
|
||||||
/ HomeR GET !free
|
/ HomeR GET !free
|
||||||
/users UsersR GET POST -- no tags, i.e. admins only
|
/users UsersR GET POST -- no tags, i.e. admins only
|
||||||
@ -63,6 +64,7 @@
|
|||||||
/info/lecturer InfoLecturerR GET !lecturer
|
/info/lecturer InfoLecturerR GET !lecturer
|
||||||
/info/data DataProtR GET !free
|
/info/data DataProtR GET !free
|
||||||
/info/allocation InfoAllocationR GET !free
|
/info/allocation InfoAllocationR GET !free
|
||||||
|
/info/glossary GlossaryR GET !free
|
||||||
/impressum ImpressumR GET !free
|
/impressum ImpressumR GET !free
|
||||||
/version VersionR GET !free
|
/version VersionR GET !free
|
||||||
|
|
||||||
@ -73,6 +75,7 @@
|
|||||||
/user/authpreds AuthPredsR GET POST !free
|
/user/authpreds AuthPredsR GET POST !free
|
||||||
/user/set-display-email SetDisplayEmailR GET POST !free
|
/user/set-display-email SetDisplayEmailR GET POST !free
|
||||||
/user/csv-options CsvOptionsR GET POST !free
|
/user/csv-options CsvOptionsR GET POST !free
|
||||||
|
/user/lang LangR POST !free
|
||||||
|
|
||||||
/exam-office ExamOfficeR !exam-office:
|
/exam-office ExamOfficeR !exam-office:
|
||||||
/ EOExamsR GET
|
/ EOExamsR GET
|
||||||
@ -141,7 +144,6 @@
|
|||||||
/invite SInviteR GET POST !ownerANDtimeANDuser-submissions
|
/invite SInviteR GET POST !ownerANDtimeANDuser-submissions
|
||||||
!/#SubmissionFileType SubArchiveR GET !owner !corrector
|
!/#SubmissionFileType SubArchiveR GET !owner !corrector
|
||||||
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector
|
!/#SubmissionFileType/*FilePath SubDownloadR GET !owner !corrector
|
||||||
/correctors SCorrR GET POST
|
|
||||||
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet
|
/iscorrector SIsCorrR GET !corrector -- Route is used to check for corrector access to this sheet
|
||||||
/pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissions
|
/pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissions
|
||||||
/corrector-invite/ SCorrInviteR GET POST
|
/corrector-invite/ SCorrInviteR GET POST
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr,
|
|||||||
import Import hiding (cancel)
|
import Import hiding (cancel)
|
||||||
import Language.Haskell.TH.Syntax (qLocation)
|
import Language.Haskell.TH.Syntax (qLocation)
|
||||||
import Network.Wai (Middleware)
|
import Network.Wai (Middleware)
|
||||||
|
import qualified Network.Wai as Wai
|
||||||
|
import qualified Network.HTTP.Types as HTTP
|
||||||
import Network.Wai.Handler.Warp (Settings, defaultSettings,
|
import Network.Wai.Handler.Warp (Settings, defaultSettings,
|
||||||
defaultShouldDisplayException,
|
defaultShouldDisplayException,
|
||||||
runSettings, runSettingsSocket, setHost,
|
runSettings, runSettingsSocket, setHost,
|
||||||
@ -48,6 +50,7 @@ import System.Directory
|
|||||||
import Jobs
|
import Jobs
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as Text
|
import qualified Data.Text.Encoding as Text
|
||||||
|
|
||||||
import Yesod.Auth.Util.PasswordStore
|
import Yesod.Auth.Util.PasswordStore
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
@ -81,12 +84,20 @@ import Network.Socket (socketPort, Socket, PortNumber)
|
|||||||
import qualified Network.Socket as Socket (close)
|
import qualified Network.Socket as Socket (close)
|
||||||
|
|
||||||
import Control.Concurrent.STM.Delay
|
import Control.Concurrent.STM.Delay
|
||||||
import Control.Monad.STM (retry)
|
|
||||||
import Control.Monad.Trans.Cont (runContT, callCC)
|
import Control.Monad.Trans.Cont (runContT, callCC)
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
import Data.Semigroup (Max(..), Min(..))
|
import Data.Semigroup (Min(..))
|
||||||
|
|
||||||
|
import qualified Prometheus.Metric.GHC as Prometheus
|
||||||
|
import qualified Prometheus
|
||||||
|
|
||||||
|
import Data.Time.Clock.POSIX
|
||||||
|
|
||||||
|
import Handler.Utils.Routes (classifyHandler)
|
||||||
|
|
||||||
|
import Data.List (cycle)
|
||||||
|
|
||||||
-- Import all relevant handler modules here.
|
-- Import all relevant handler modules here.
|
||||||
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
-- (HPack takes care to add new modules to our cabal file nowadays.)
|
||||||
@ -111,6 +122,7 @@ import Handler.Health
|
|||||||
import Handler.Exam
|
import Handler.Exam
|
||||||
import Handler.Allocation
|
import Handler.Allocation
|
||||||
import Handler.ExamOffice
|
import Handler.ExamOffice
|
||||||
|
import Handler.Metrics
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
@ -124,6 +136,8 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
|
|||||||
-- migrations handled by Yesod.
|
-- migrations handled by Yesod.
|
||||||
makeFoundation :: (MonadResource m, MonadUnliftIO m) => AppSettings -> m UniWorX
|
makeFoundation :: (MonadResource m, MonadUnliftIO m) => AppSettings -> m UniWorX
|
||||||
makeFoundation appSettings'@AppSettings{..} = do
|
makeFoundation appSettings'@AppSettings{..} = do
|
||||||
|
void $ Prometheus.register Prometheus.ghcMetrics
|
||||||
|
|
||||||
-- Some basic initializations: HTTP connection manager, logger, and static
|
-- Some basic initializations: HTTP connection manager, logger, and static
|
||||||
-- subsite.
|
-- subsite.
|
||||||
appHttpManager <- newManager
|
appHttpManager <- newManager
|
||||||
@ -290,7 +304,39 @@ makeApplication foundation = liftIO $ do
|
|||||||
logWare <- makeLogWare foundation
|
logWare <- makeLogWare foundation
|
||||||
-- Create the WAI application and apply middlewares
|
-- Create the WAI application and apply middlewares
|
||||||
appPlain <- toWaiAppPlain foundation
|
appPlain <- toWaiAppPlain foundation
|
||||||
return $ logWare $ defaultMiddlewaresNoLogging appPlain
|
return . prometheusMiddleware . logWare $ defaultMiddlewaresNoLogging appPlain
|
||||||
|
where
|
||||||
|
prometheusMiddleware :: Middleware
|
||||||
|
prometheusMiddleware app req respond' = do
|
||||||
|
start <- getPOSIXTime
|
||||||
|
app req $ \res -> do
|
||||||
|
end <- getPOSIXTime
|
||||||
|
let method = decodeUtf8 $ Wai.requestMethod req
|
||||||
|
status = tshow . HTTP.statusCode $ Wai.responseStatus res
|
||||||
|
route :: Maybe (Route UniWorX)
|
||||||
|
route = parseRoute ( Wai.pathInfo req
|
||||||
|
, over (mapped . _2) (fromMaybe "") . HTTP.queryToQueryText $ Wai.queryString req
|
||||||
|
)
|
||||||
|
handler' = pack . classifyHandler <$> route
|
||||||
|
|
||||||
|
labels :: Prometheus.Label3
|
||||||
|
labels = (fromMaybe "n/a" handler', method, status)
|
||||||
|
Prometheus.withLabel requestLatency labels . flip Prometheus.observe . realToFrac $ end - start
|
||||||
|
|
||||||
|
respond' res
|
||||||
|
|
||||||
|
{-# NOINLINE requestLatency #-}
|
||||||
|
requestLatency :: Prometheus.Vector Prometheus.Label3 Prometheus.Histogram
|
||||||
|
requestLatency = Prometheus.unsafeRegister
|
||||||
|
$ Prometheus.vector ("handler", "method", "status")
|
||||||
|
$ Prometheus.histogram info buckets
|
||||||
|
where info = Prometheus.Info "http_request_duration_seconds"
|
||||||
|
"HTTP request latency"
|
||||||
|
buckets = map fromRational . takeWhile (<= 500) . go 50e-6 $ cycle [2, 2, 2.5]
|
||||||
|
where
|
||||||
|
go n [] = [n]
|
||||||
|
go n (f:fs) = n : go (f * n) fs
|
||||||
|
|
||||||
|
|
||||||
makeLogWare :: MonadIO m => UniWorX -> m Middleware
|
makeLogWare :: MonadIO m => UniWorX -> m Middleware
|
||||||
makeLogWare app = do
|
makeLogWare app = do
|
||||||
@ -320,13 +366,22 @@ makeLogWare app = do
|
|||||||
logWare <- either mkLogWare return lookupRes
|
logWare <- either mkLogWare return lookupRes
|
||||||
logWare wai req fin
|
logWare wai req fin
|
||||||
|
|
||||||
|
data ReadySince = MkReadySince
|
||||||
|
|
||||||
-- | Warp settings for the given foundation value.
|
-- | Warp settings for the given foundation value.
|
||||||
warpSettings :: UniWorX -> Settings
|
warpSettings :: UniWorX -> Settings
|
||||||
warpSettings foundation = defaultSettings
|
warpSettings foundation = defaultSettings
|
||||||
& setBeforeMainLoop (runAppLoggingT foundation $ do
|
& setBeforeMainLoop (runAppLoggingT foundation $ do
|
||||||
let notifyReady = do
|
let notifyReady = do
|
||||||
$logInfoS "setup" "Ready"
|
$logInfoS "setup" "Ready"
|
||||||
void $ liftIO Systemd.notifyReady
|
void . liftIO $ do
|
||||||
|
void . Prometheus.register . readyMetric =<< getCurrentTime
|
||||||
|
Systemd.notifyReady
|
||||||
|
readyMetric ts = Prometheus.Metric $ return (MkReadySince, collectReadySince)
|
||||||
|
where
|
||||||
|
collectReadySince = return [Prometheus.SampleGroup info Prometheus.GaugeType [Prometheus.Sample "ready_time" [] sample]]
|
||||||
|
info = Prometheus.Info "ready_time" "POSIXTime this Uni2work-instance became ready"
|
||||||
|
sample = encodeUtf8 . tshow . (realToFrac :: POSIXTime -> Nano) $ utcTimeToPOSIXSeconds ts
|
||||||
if
|
if
|
||||||
| foundation ^. _appHealthCheckDelayNotify
|
| foundation ^. _appHealthCheckDelayNotify
|
||||||
-> void . forkIO $ do
|
-> void . forkIO $ do
|
||||||
@ -425,39 +480,46 @@ appMain = runResourceT $ do
|
|||||||
case watchdogMicroSec of
|
case watchdogMicroSec of
|
||||||
Just wInterval
|
Just wInterval
|
||||||
| maybe True (== myProcessID) watchdogProcess
|
| maybe True (== myProcessID) watchdogProcess
|
||||||
-> let notifyWatchdog :: IO ()
|
-> let notifyWatchdog :: forall a. IO a
|
||||||
notifyWatchdog = runAppLoggingT foundation $ go Nothing
|
notifyWatchdog = runAppLoggingT foundation $ go Nothing
|
||||||
where
|
where
|
||||||
go pStatus = do
|
go :: Maybe (Set (UTCTime, HealthReport)) -> LoggingT IO a
|
||||||
d <- liftIO . newDelay . floor $ wInterval % 2
|
go pResults = do
|
||||||
|
let delay = floor $ wInterval % 2
|
||||||
|
d <- liftIO $ newDelay delay
|
||||||
|
|
||||||
status <- atomically $ asum
|
$logDebugS "Notify" $ "Waiting up to " <> tshow delay <> "µs..."
|
||||||
[ Nothing <$ waitDelay d
|
mResults <- atomically $ asum
|
||||||
, Just <$> do
|
[ pResults <$ waitDelay d
|
||||||
|
, do
|
||||||
results <- readTVar $ foundation ^. _appHealthReport
|
results <- readTVar $ foundation ^. _appHealthReport
|
||||||
case fromNullable results of
|
guardOn (pResults /= Just results) $ Just results
|
||||||
Nothing -> retry
|
|
||||||
Just rs -> do
|
|
||||||
let status = ofoldMap1 (Max *** Min . healthReportStatus) rs
|
|
||||||
guard $ pStatus /= Just status
|
|
||||||
return status
|
|
||||||
]
|
]
|
||||||
|
|
||||||
case status of
|
$logDebugS "Notify" "Checking for status/watchdog..."
|
||||||
Just (_, Min status') -> do
|
(*> go mResults) . void . runMaybeT $ do
|
||||||
$logInfoS "NotifyStatus" $ toPathPiece status'
|
results <- hoistMaybe mResults
|
||||||
liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status'
|
|
||||||
Nothing -> return ()
|
|
||||||
|
|
||||||
case status of
|
Min status <- hoistMaybe $ ofoldMap1 (Min . healthReportStatus . view _2) <$> fromNullable results
|
||||||
Just (_, Min HealthSuccess) -> do
|
$logInfoS "NotifyStatus" $ toPathPiece status
|
||||||
$logInfoS "NotifyWatchdog" "Notify"
|
liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status
|
||||||
liftIO $ void Systemd.notifyWatchdog
|
|
||||||
_other -> return ()
|
|
||||||
|
|
||||||
go status
|
now <- liftIO getCurrentTime
|
||||||
in void $ allocateLinkedAsync notifyWatchdog
|
iforM_ (foundation ^. _appHealthCheckInterval) . curry $ \case
|
||||||
_other -> return ()
|
(_, Nothing) -> return ()
|
||||||
|
(hc, Just interval) -> do
|
||||||
|
lastSuccess <- hoistMaybe $ results
|
||||||
|
& Set.filter (\(_, rep) -> classifyHealthReport rep == hc)
|
||||||
|
& Set.filter (\(_, rep) -> healthReportStatus rep >= HealthSuccess)
|
||||||
|
& Set.mapMonotonic (view _1)
|
||||||
|
& Set.lookupMax
|
||||||
|
guard $ lastSuccess > addUTCTime (negate interval) now
|
||||||
|
$logInfoS "NotifyWatchdog" "Notify"
|
||||||
|
liftIO $ void Systemd.notifyWatchdog
|
||||||
|
in do
|
||||||
|
$logDebugS "Notify" "Spawning notify thread..."
|
||||||
|
void $ allocateLinkedAsync notifyWatchdog
|
||||||
|
_other -> $logWarnS "Notify" "Not sending notifications of status/poking watchdog"
|
||||||
|
|
||||||
let runWarp socket = runSettingsSocket (warpSettings foundation) socket app
|
let runWarp socket = runSettingsSocket (warpSettings foundation) socket app
|
||||||
case sockets of
|
case sockets of
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import qualified Data.CaseInsensitive as CI
|
|||||||
|
|
||||||
|
|
||||||
data DummyMessage = MsgDummyIdent
|
data DummyMessage = MsgDummyIdent
|
||||||
|
| MsgDummyIdentPlaceholder
|
||||||
| MsgDummyNoFormData
|
| MsgDummyNoFormData
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
@ -24,7 +25,9 @@ dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
|
|||||||
, Button (HandlerSite m) ButtonSubmit
|
, Button (HandlerSite m) ButtonSubmit
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
) => AForm m (CI Text)
|
) => AForm m (CI Text)
|
||||||
dummyForm = areq (ciField & addDatalist userList) (fslI MsgDummyIdent & noAutocomplete) Nothing
|
dummyForm = wFormToAForm $ do
|
||||||
|
mr <- getMessageRender
|
||||||
|
aFormToWForm $ areq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing
|
||||||
where
|
where
|
||||||
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
|
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
|
||||||
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
|
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
|
||||||
|
|||||||
@ -36,6 +36,7 @@ data CampusLogin = CampusLogin
|
|||||||
data CampusMessage = MsgCampusIdentPlaceholder
|
data CampusMessage = MsgCampusIdentPlaceholder
|
||||||
| MsgCampusIdent
|
| MsgCampusIdent
|
||||||
| MsgCampusPassword
|
| MsgCampusPassword
|
||||||
|
| MsgCampusPasswordPlaceholder
|
||||||
| MsgCampusSubmit
|
| MsgCampusSubmit
|
||||||
| MsgCampusInvalidCredentials
|
| MsgCampusInvalidCredentials
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
@ -130,7 +131,7 @@ campusForm = do
|
|||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
ident <- wreq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing
|
ident <- wreq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing
|
||||||
password <- wreq passwordField (fslI MsgCampusPassword) Nothing
|
password <- wreq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing
|
||||||
|
|
||||||
return $ CampusLogin
|
return $ CampusLogin
|
||||||
<$> ident
|
<$> ident
|
||||||
|
|||||||
@ -22,7 +22,9 @@ data HashLogin = HashLogin
|
|||||||
} deriving (Generic, Typeable)
|
} deriving (Generic, Typeable)
|
||||||
|
|
||||||
data PWHashMessage = MsgPWHashIdent
|
data PWHashMessage = MsgPWHashIdent
|
||||||
|
| MsgPWHashIdentPlaceholder
|
||||||
| MsgPWHashPassword
|
| MsgPWHashPassword
|
||||||
|
| MsgPWHashPasswordPlaceholder
|
||||||
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
|
||||||
@ -30,9 +32,11 @@ hashForm :: ( RenderMessage (HandlerSite m) FormMessage
|
|||||||
, RenderMessage (HandlerSite m) PWHashMessage
|
, RenderMessage (HandlerSite m) PWHashMessage
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
) => AForm m HashLogin
|
) => AForm m HashLogin
|
||||||
hashForm = HashLogin
|
hashForm = wFormToAForm $ do
|
||||||
<$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing
|
mr <- getMessageRender
|
||||||
<*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing
|
aFormToWForm $ HashLogin
|
||||||
|
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing
|
||||||
|
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing
|
||||||
|
|
||||||
|
|
||||||
hashLogin :: forall site.
|
hashLogin :: forall site.
|
||||||
|
|||||||
12
src/Cron.hs
12
src/Cron.hs
@ -155,7 +155,7 @@ nextCronMatch :: TZ -- ^ Timezone of the `Cron`-Entry
|
|||||||
-> UTCTime -- ^ Current time, used only for `CronCalendar`
|
-> UTCTime -- ^ Current time, used only for `CronCalendar`
|
||||||
-> Cron
|
-> Cron
|
||||||
-> CronNextMatch UTCTime
|
-> CronNextMatch UTCTime
|
||||||
nextCronMatch tz mPrev prec now c@Cron{..} = case notAfter of
|
nextCronMatch tz mPrev prec now c@Cron{..} = onlyOnceWithinPrec $ case notAfter of
|
||||||
MatchAsap -> MatchNone
|
MatchAsap -> MatchNone
|
||||||
MatchAt ts
|
MatchAt ts
|
||||||
| MatchAt ts' <- nextMatch
|
| MatchAt ts' <- nextMatch
|
||||||
@ -165,6 +165,16 @@ nextCronMatch tz mPrev prec now c@Cron{..} = case notAfter of
|
|||||||
| otherwise -> MatchNone
|
| otherwise -> MatchNone
|
||||||
MatchNone -> nextMatch
|
MatchNone -> nextMatch
|
||||||
where
|
where
|
||||||
|
onlyOnceWithinPrec sched = case mPrev of
|
||||||
|
Nothing -> sched
|
||||||
|
Just prevT -> case sched of
|
||||||
|
MatchAsap
|
||||||
|
| now >= addUTCTime prec prevT -> MatchAsap
|
||||||
|
| otherwise -> MatchAt $ addUTCTime prec prevT
|
||||||
|
MatchAt ts -> let ts' = max ts $ addUTCTime prec prevT
|
||||||
|
in if | ts' <= addUTCTime prec now -> MatchAsap
|
||||||
|
| otherwise -> MatchAt ts'
|
||||||
|
MatchNone -> MatchNone
|
||||||
notAfter
|
notAfter
|
||||||
| Right c' <- cronNotAfter
|
| Right c' <- cronNotAfter
|
||||||
, Just ref <- notAfterRef
|
, Just ref <- notAfterRef
|
||||||
|
|||||||
20
src/Data/Universe/Instances/Reverse/WithIndex.hs
Normal file
20
src/Data/Universe/Instances/Reverse/WithIndex.hs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Data.Universe.Instances.Reverse.WithIndex
|
||||||
|
(
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import Data.Universe
|
||||||
|
import Control.Lens.Indexed
|
||||||
|
|
||||||
|
import Data.Universe.Instances.Reverse ()
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
|
instance Finite a => FoldableWithIndex a ((->) a) where
|
||||||
|
ifoldMap f g = fold [ f x (g x) | x <- universeF ]
|
||||||
|
instance (Ord a, Finite a) => TraversableWithIndex a ((->) a) where
|
||||||
|
itraverse f g = (Map.!) . Map.fromList <$> sequenceA [ (x, ) <$> f x (g x) | x <- universeF ]
|
||||||
@ -19,6 +19,7 @@ module Database.Esqueleto.Utils
|
|||||||
, sha256
|
, sha256
|
||||||
, maybe
|
, maybe
|
||||||
, SqlProject(..)
|
, SqlProject(..)
|
||||||
|
, (->.)
|
||||||
, module Database.Esqueleto.Utils.TH
|
, module Database.Esqueleto.Utils.TH
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@ -246,3 +247,6 @@ instance (PersistEntity val, PersistField typ) => SqlProject val typ (E.Entity v
|
|||||||
instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.Entity val)) (Maybe typ) where
|
instance (PersistEntity val, PersistField typ) => SqlProject val typ (Maybe (E.Entity val)) (Maybe typ) where
|
||||||
sqlProject = (E.?.)
|
sqlProject = (E.?.)
|
||||||
unSqlProject _ _ = Just
|
unSqlProject _ _ = Just
|
||||||
|
|
||||||
|
(->.) :: E.SqlExpr (E.Value a) -> Text -> E.SqlExpr (E.Value b)
|
||||||
|
(->.) expr t = E.unsafeSqlBinOp "->" expr $ E.val t
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
313
src/Foundation/I18n.hs
Normal file
313
src/Foundation/I18n.hs
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
module Foundation.I18n
|
||||||
|
( UniWorXMessage(..)
|
||||||
|
, ShortTermIdentifier(..)
|
||||||
|
, MsgLanguage(..)
|
||||||
|
, ShortSex(..)
|
||||||
|
, SheetTypeHeader(..)
|
||||||
|
, ShortStudyDegree(..)
|
||||||
|
, ShortStudyTerms(..)
|
||||||
|
, StudyDegreeTerm(..)
|
||||||
|
, ShortStudyFieldType(..)
|
||||||
|
, StudyDegreeTermType(..)
|
||||||
|
, ErrorResponseTitle(..)
|
||||||
|
, UniWorXMessages(..)
|
||||||
|
, uniworxMessages
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Foundation.Type
|
||||||
|
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
import Auth.LDAP
|
||||||
|
import Auth.PWHash
|
||||||
|
import Auth.Dummy
|
||||||
|
|
||||||
|
import Data.CaseInsensitive (original, mk)
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Utils.Form
|
||||||
|
|
||||||
|
import Text.Shakespeare.Text (st)
|
||||||
|
|
||||||
|
import GHC.Exts (IsList(..))
|
||||||
|
|
||||||
|
|
||||||
|
pluralDE :: (Eq a, Num a)
|
||||||
|
=> a -- ^ Count
|
||||||
|
-> Text -- ^ Singular
|
||||||
|
-> Text -- ^ Plural
|
||||||
|
-> Text
|
||||||
|
pluralDE num singularForm pluralForm
|
||||||
|
| num == 1 = singularForm
|
||||||
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
noneOneMoreDE :: (Eq a, Num a)
|
||||||
|
=> a -- ^ Count
|
||||||
|
-> Text -- ^ None
|
||||||
|
-> Text -- ^ Singular
|
||||||
|
-> Text -- ^ Plural
|
||||||
|
-> Text
|
||||||
|
noneOneMoreDE num noneText singularForm pluralForm
|
||||||
|
| num == 0 = noneText
|
||||||
|
| num == 1 = singularForm
|
||||||
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
-- noneMoreDE :: (Eq a, Num a)
|
||||||
|
-- => a -- ^ Count
|
||||||
|
-- -> Text -- ^ None
|
||||||
|
-- -> Text -- ^ Some
|
||||||
|
-- -> Text
|
||||||
|
-- noneMoreDE num noneText someText
|
||||||
|
-- | num == 0 = noneText
|
||||||
|
-- | otherwise = someText
|
||||||
|
|
||||||
|
pluralEN :: (Eq a, Num a)
|
||||||
|
=> a -- ^ Count
|
||||||
|
-> Text -- ^ Singular
|
||||||
|
-> Text -- ^ Plural
|
||||||
|
-> Text
|
||||||
|
pluralEN num singularForm pluralForm
|
||||||
|
| num == 1 = singularForm
|
||||||
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
noneOneMoreEN :: (Eq a, Num a)
|
||||||
|
=> a -- ^ Count
|
||||||
|
-> Text -- ^ None
|
||||||
|
-> Text -- ^ Singular
|
||||||
|
-> Text -- ^ Plural
|
||||||
|
-> Text
|
||||||
|
noneOneMoreEN num noneText singularForm pluralForm
|
||||||
|
| num == 0 = noneText
|
||||||
|
| num == 1 = singularForm
|
||||||
|
| otherwise = pluralForm
|
||||||
|
|
||||||
|
-- noneMoreEN :: (Eq a, Num a)
|
||||||
|
-- => a -- ^ Count
|
||||||
|
-- -> Text -- ^ None
|
||||||
|
-- -> Text -- ^ Some
|
||||||
|
-- -> Text
|
||||||
|
-- noneMoreEN num noneText someText
|
||||||
|
-- | num == 0 = noneText
|
||||||
|
-- | otherwise = someText
|
||||||
|
|
||||||
|
ordinalEN :: ToMessage a
|
||||||
|
=> a
|
||||||
|
-> Text
|
||||||
|
ordinalEN (toMessage -> numStr) = case lastChar of
|
||||||
|
Just '1' -> [st|#{numStr}st|]
|
||||||
|
Just '2' -> [st|#{numStr}nd|]
|
||||||
|
Just '3' -> [st|#{numStr}rd|]
|
||||||
|
_other -> [st|#{numStr}th|]
|
||||||
|
where
|
||||||
|
lastChar = last <$> fromNullable numStr
|
||||||
|
|
||||||
|
|
||||||
|
-- Convenience Type for Messages, since Yesod messages cannot deal with compound type identifiers
|
||||||
|
type IntMaybe = Maybe Int
|
||||||
|
|
||||||
|
-- | Convenience function for i18n messages definitions
|
||||||
|
maybeToMessage :: ToMessage m => Text -> Maybe m -> Text -> Text
|
||||||
|
maybeToMessage _ Nothing _ = mempty
|
||||||
|
maybeToMessage before (Just x) after = before <> toMessage x <> after
|
||||||
|
|
||||||
|
-- Messages creates type UniWorXMessage and RenderMessage UniWorX instance
|
||||||
|
mkMessage "UniWorX" "messages/uniworx" "de-de-formal"
|
||||||
|
mkMessageVariant "UniWorX" "Campus" "messages/campus" "de"
|
||||||
|
mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de"
|
||||||
|
mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de"
|
||||||
|
mkMessageVariant "UniWorX" "Button" "messages/button" "de"
|
||||||
|
mkMessageVariant "UniWorX" "Frontend" "messages/frontend" "de-de-formal"
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX TermIdentifier where
|
||||||
|
renderMessage foundation ls TermIdentifier{..} = case season of
|
||||||
|
Summer -> renderMessage' $ MsgSummerTerm year
|
||||||
|
Winter -> renderMessage' $ MsgWinterTerm year
|
||||||
|
where renderMessage' = renderMessage foundation ls
|
||||||
|
|
||||||
|
newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier
|
||||||
|
deriving stock (Eq, Ord, Read, Show)
|
||||||
|
instance RenderMessage UniWorX ShortTermIdentifier where
|
||||||
|
renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of
|
||||||
|
Summer -> renderMessage' $ MsgSummerTermShort year
|
||||||
|
Winter -> renderMessage' $ MsgWinterTermShort year
|
||||||
|
where renderMessage' = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX String where
|
||||||
|
renderMessage f ls str = renderMessage f ls $ Text.pack str
|
||||||
|
|
||||||
|
-- TODO: raw number representation; instead, display e.g. 1000 as 1.000 or 1,000 or ... (language-dependent!)
|
||||||
|
instance RenderMessage UniWorX Int where
|
||||||
|
renderMessage f ls = renderMessage f ls . tshow
|
||||||
|
instance RenderMessage UniWorX Int64 where
|
||||||
|
renderMessage f ls = renderMessage f ls . tshow
|
||||||
|
instance RenderMessage UniWorX Integer where
|
||||||
|
renderMessage f ls = renderMessage f ls . tshow
|
||||||
|
instance RenderMessage UniWorX Natural where
|
||||||
|
renderMessage f ls = renderMessage f ls . tshow
|
||||||
|
|
||||||
|
instance HasResolution a => RenderMessage UniWorX (Fixed a) where
|
||||||
|
renderMessage f ls = renderMessage f ls . showFixed True
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX Load where
|
||||||
|
renderMessage foundation ls = renderMessage foundation ls . \case
|
||||||
|
Load { byTutorial = Nothing , byProportion = p } -> MsgCorByProportionOnly p
|
||||||
|
Load { byTutorial = Just True , byProportion = p } -> MsgCorByProportionIncludingTutorial p
|
||||||
|
Load { byTutorial = Just False, byProportion = p } -> MsgCorByProportionExcludingTutorial p
|
||||||
|
|
||||||
|
newtype MsgLanguage = MsgLanguage Lang
|
||||||
|
deriving stock (Eq, Ord, Show, Read)
|
||||||
|
instance RenderMessage UniWorX MsgLanguage where
|
||||||
|
renderMessage foundation ls (MsgLanguage lang@(map mk . Text.splitOn "-" -> lang'))
|
||||||
|
| ("de" : "DE" : _) <- lang' = mr MsgGermanGermany
|
||||||
|
| ("de" : _) <- lang' = mr MsgGerman
|
||||||
|
| ("en" : "EU" : _) <- lang' = mr MsgEnglishEurope
|
||||||
|
| ("en" : _) <- lang' = mr MsgEnglish
|
||||||
|
| otherwise = lang
|
||||||
|
where
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
|
||||||
|
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
|
||||||
|
embedRenderMessage ''UniWorX ''StudyFieldType id
|
||||||
|
embedRenderMessage ''UniWorX ''SheetFileType id
|
||||||
|
embedRenderMessage ''UniWorX ''SubmissionFileType id
|
||||||
|
embedRenderMessage ''UniWorX ''CorrectorState id
|
||||||
|
embedRenderMessage ''UniWorX ''RatingException id
|
||||||
|
embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>)
|
||||||
|
embedRenderMessage ''UniWorX ''SheetGrading ("SheetGrading" <>)
|
||||||
|
embedRenderMessage ''UniWorX ''AuthTag $ ("AuthTag" <>) . concat . drop 1 . splitCamel
|
||||||
|
embedRenderMessage ''UniWorX ''EncodedSecretBoxException id
|
||||||
|
embedRenderMessage ''UniWorX ''LecturerType id
|
||||||
|
embedRenderMessage ''UniWorX ''SubmissionModeDescr
|
||||||
|
$ let verbMap [_, _, "None"] = "NoSubmissions"
|
||||||
|
verbMap [_, _, v] = v <> "Submissions"
|
||||||
|
verbMap _ = error "Invalid number of verbs"
|
||||||
|
in verbMap . splitCamel
|
||||||
|
embedRenderMessage ''UniWorX ''UploadModeDescr id
|
||||||
|
embedRenderMessage ''UniWorX ''SecretJSONFieldException id
|
||||||
|
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
|
||||||
|
embedRenderMessage ''UniWorX ''SchoolFunction id
|
||||||
|
embedRenderMessage ''UniWorX ''CsvPreset id
|
||||||
|
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
|
||||||
|
embedRenderMessage ''UniWorX ''FavouriteReason id
|
||||||
|
embedRenderMessage ''UniWorX ''Sex id
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''AuthenticationMode id
|
||||||
|
|
||||||
|
newtype ShortSex = ShortSex Sex
|
||||||
|
embedRenderMessageVariant ''UniWorX ''ShortSex ("Short" <>)
|
||||||
|
|
||||||
|
newtype SheetTypeHeader = SheetTypeHeader SheetType
|
||||||
|
embedRenderMessageVariant ''UniWorX ''SheetTypeHeader ("SheetType" <>)
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX SheetType where
|
||||||
|
renderMessage foundation ls sheetType = case sheetType of
|
||||||
|
NotGraded -> mr $ SheetTypeHeader NotGraded
|
||||||
|
other -> mr (grading other) <> ", " <> mr (SheetTypeHeader other)
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX StudyDegree where
|
||||||
|
renderMessage _found _ls StudyDegree{..} = fromMaybe (tshow studyDegreeKey) (studyDegreeName <|> studyDegreeShorthand)
|
||||||
|
|
||||||
|
newtype ShortStudyDegree = ShortStudyDegree StudyDegree
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX ShortStudyDegree where
|
||||||
|
renderMessage _found _ls (ShortStudyDegree StudyDegree{..}) = fromMaybe (tshow studyDegreeKey) studyDegreeShorthand
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX StudyTerms where
|
||||||
|
renderMessage _found _ls StudyTerms{..} = fromMaybe (tshow studyTermsKey) (studyTermsName <|> studyTermsShorthand)
|
||||||
|
|
||||||
|
newtype ShortStudyTerms = ShortStudyTerms StudyTerms
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX ShortStudyTerms where
|
||||||
|
renderMessage _found _ls (ShortStudyTerms StudyTerms{..}) = fromMaybe (tshow studyTermsKey) studyTermsShorthand
|
||||||
|
|
||||||
|
data StudyDegreeTerm = StudyDegreeTerm StudyDegree StudyTerms
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX StudyDegreeTerm where
|
||||||
|
renderMessage foundation ls (StudyDegreeTerm deg trm) = mr trm <> " (" <> mr (ShortStudyDegree deg) <> ")"
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
newtype ShortStudyFieldType = ShortStudyFieldType StudyFieldType
|
||||||
|
embedRenderMessageVariant ''UniWorX ''ShortStudyFieldType ("Short" <>)
|
||||||
|
|
||||||
|
data StudyDegreeTermType = StudyDegreeTermType StudyDegree StudyTerms StudyFieldType
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX StudyDegreeTermType where
|
||||||
|
renderMessage foundation ls (StudyDegreeTermType deg trm typ) = mr trm <> " (" <> mr (ShortStudyDegree deg) <> ", " <> mr (ShortStudyFieldType typ) <> ")"
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX ExamGrade where
|
||||||
|
renderMessage _ _ = pack . (showFixed False :: Deci -> String) . fromRational . review numberGrade
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX ExamPassed where
|
||||||
|
renderMessage foundation ls = \case
|
||||||
|
ExamPassed True -> mr MsgExamPassed
|
||||||
|
ExamPassed False -> mr MsgExamNotPassed
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX a => RenderMessage UniWorX (ExamResult' a) where
|
||||||
|
renderMessage foundation ls = \case
|
||||||
|
ExamAttended{..} -> mr examResult
|
||||||
|
ExamNoShow -> mr MsgExamResultNoShow
|
||||||
|
ExamVoided -> mr MsgExamResultVoided
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX (Either ExamPassed ExamGrade) where
|
||||||
|
renderMessage foundation ls = either mr mr
|
||||||
|
where
|
||||||
|
mr :: RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage foundation ls
|
||||||
|
|
||||||
|
-- ToMessage instances for converting raw numbers to Text (no internationalization)
|
||||||
|
|
||||||
|
instance ToMessage Int where
|
||||||
|
toMessage = tshow
|
||||||
|
instance ToMessage Int64 where
|
||||||
|
toMessage = tshow
|
||||||
|
instance ToMessage Integer where
|
||||||
|
toMessage = tshow
|
||||||
|
instance ToMessage Natural where
|
||||||
|
toMessage = tshow
|
||||||
|
|
||||||
|
instance HasResolution a => ToMessage (Fixed a) where
|
||||||
|
toMessage = toMessage . showFixed True
|
||||||
|
|
||||||
|
-- Do not use toMessage on Rationals and round them automatically. Instead, use rationalToFixed3 (declared in src/Utils.hs) to convert a Rational to Fixed E3!
|
||||||
|
-- instance ToMessage Rational where
|
||||||
|
-- toMessage = toMessage . fromRational'
|
||||||
|
-- where fromRational' = fromRational :: Rational -> Fixed E3
|
||||||
|
|
||||||
|
|
||||||
|
newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse
|
||||||
|
embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>)
|
||||||
|
|
||||||
|
newtype UniWorXMessages = UniWorXMessages [SomeMessage UniWorX]
|
||||||
|
deriving stock (Generic, Typeable)
|
||||||
|
deriving newtype (Semigroup, Monoid)
|
||||||
|
|
||||||
|
instance IsList UniWorXMessages where
|
||||||
|
type Item UniWorXMessages = SomeMessage UniWorX
|
||||||
|
fromList = UniWorXMessages
|
||||||
|
toList (UniWorXMessages msgs) = msgs
|
||||||
|
|
||||||
|
instance RenderMessage UniWorX UniWorXMessages where
|
||||||
|
renderMessage foundation ls (UniWorXMessages msgs) =
|
||||||
|
Text.unwords $ map (renderMessage foundation ls) msgs
|
||||||
|
|
||||||
|
uniworxMessages :: [UniWorXMessage] -> UniWorXMessages
|
||||||
|
uniworxMessages = UniWorXMessages . map SomeMessage
|
||||||
10
src/Foundation/Routes.hs
Normal file
10
src/Foundation/Routes.hs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module Foundation.Routes
|
||||||
|
( uniworxRoutes
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
import Yesod.Routes.TH.Types (ResourceTree)
|
||||||
|
|
||||||
|
|
||||||
|
uniworxRoutes :: [ResourceTree String]
|
||||||
|
uniworxRoutes = $(parseRoutesFile "routes")
|
||||||
58
src/Foundation/Type.hs
Normal file
58
src/Foundation/Type.hs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
module Foundation.Type
|
||||||
|
( UniWorX(..)
|
||||||
|
, SMTPPool
|
||||||
|
, _appSettings', _appStatic, _appConnPool, _appSmtpPool, _appLdapPool, _appWidgetMemcached, _appHttpManager, _appLogger, _appLogSettings, _appCryptoIDKey, _appClusterID, _appInstanceID, _appJobState, _appSessionKey, _appSecretBoxKey, _appJSONWebKeySet, _appHealthReport
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
import Database.Persist.Sql (ConnectionPool)
|
||||||
|
|
||||||
|
import qualified Web.ClientSession as ClientSession
|
||||||
|
|
||||||
|
import Jobs.Types
|
||||||
|
|
||||||
|
import Yesod.Core.Types (Logger)
|
||||||
|
|
||||||
|
import Data.Set (Set)
|
||||||
|
|
||||||
|
import qualified Crypto.Saltine.Core.SecretBox as SecretBox
|
||||||
|
import qualified Jose.Jwk as Jose
|
||||||
|
|
||||||
|
import qualified Database.Memcached.Binary.IO as Memcached
|
||||||
|
|
||||||
|
|
||||||
|
type SMTPPool = Pool SMTPConnection
|
||||||
|
|
||||||
|
-- | The foundation datatype for your application. This can be a good place to
|
||||||
|
-- keep settings and values requiring initialization before your application
|
||||||
|
-- starts running, such as database connections. Every handler will have
|
||||||
|
-- access to the data present here.
|
||||||
|
data UniWorX = UniWorX
|
||||||
|
{ appSettings' :: AppSettings
|
||||||
|
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
|
||||||
|
, appConnPool :: ConnectionPool -- ^ Database connection pool.
|
||||||
|
, appSmtpPool :: Maybe SMTPPool
|
||||||
|
, appLdapPool :: Maybe LdapPool
|
||||||
|
, appWidgetMemcached :: Maybe Memcached.Connection -- ^ Actually a proper pool
|
||||||
|
, appHttpManager :: Manager
|
||||||
|
, appLogger :: (ReleaseKey, TVar Logger)
|
||||||
|
, appLogSettings :: TVar LogSettings
|
||||||
|
, appCryptoIDKey :: CryptoIDKey
|
||||||
|
, appClusterID :: ClusterId
|
||||||
|
, appInstanceID :: InstanceId
|
||||||
|
, appJobState :: TMVar JobState
|
||||||
|
, appSessionKey :: ClientSession.Key
|
||||||
|
, appSecretBoxKey :: SecretBox.Key
|
||||||
|
, appJSONWebKeySet :: Jose.JwkSet
|
||||||
|
, appHealthReport :: TVar (Set (UTCTime, HealthReport))
|
||||||
|
}
|
||||||
|
|
||||||
|
makeLenses_ ''UniWorX
|
||||||
|
instance HasInstanceID UniWorX InstanceId where
|
||||||
|
instanceID = _appInstanceID
|
||||||
|
instance HasJSONWebKeySet UniWorX Jose.JwkSet where
|
||||||
|
jsonWebKeySet = _appJSONWebKeySet
|
||||||
|
instance HasHttpManager UniWorX Manager where
|
||||||
|
httpManager = _appHttpManager
|
||||||
|
instance HasAppSettings UniWorX where
|
||||||
|
appSettings = _appSettings'
|
||||||
@ -4,6 +4,8 @@ module Handler.Admin
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
import Handler.Admin.Test as Handler.Admin
|
import Handler.Admin.Test as Handler.Admin
|
||||||
import Handler.Admin.ErrorMessage as Handler.Admin
|
import Handler.Admin.ErrorMessage as Handler.Admin
|
||||||
import Handler.Admin.StudyFeatures as Handler.Admin
|
import Handler.Admin.StudyFeatures as Handler.Admin
|
||||||
@ -13,8 +15,4 @@ getAdminR :: Handler Html
|
|||||||
getAdminR =
|
getAdminR =
|
||||||
siteLayoutMsg MsgAdminHeading $ do
|
siteLayoutMsg MsgAdminHeading $ do
|
||||||
setTitleI MsgAdminHeading
|
setTitleI MsgAdminHeading
|
||||||
[whamlet|
|
i18n MsgAdminPageEmpty
|
||||||
This shall become the Administrators' overview page.
|
|
||||||
Its current purpose is to provide links to some important admin functions
|
|
||||||
|]
|
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ emailTestForm :: AForm (HandlerFor UniWorX) (Email, MailContext)
|
|||||||
emailTestForm = (,)
|
emailTestForm = (,)
|
||||||
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing
|
<$> areq emailField (fslI MsgMailTestFormEmail) Nothing
|
||||||
<*> ( MailContext
|
<*> ( MailContext
|
||||||
<$> (MailLanguages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing)
|
<$> (Languages <$> areq (reorderField appLanguagesOpts) (fslI MsgMailTestFormLanguages) Nothing)
|
||||||
<*> (toMailDateTimeFormat
|
<*> (toMailDateTimeFormat
|
||||||
<$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing
|
<$> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) Nothing
|
||||||
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatDate) (fslI MsgDateFormat) Nothing
|
||||||
@ -54,7 +54,7 @@ emailTestForm = (,)
|
|||||||
makeDemoForm :: Int -> Form (Int,Bool,Double)
|
makeDemoForm :: Int -> Form (Int,Bool,Double)
|
||||||
makeDemoForm n = identifyForm ("adminTestForm" :: Text) $ \html -> do
|
makeDemoForm n = identifyForm ("adminTestForm" :: Text) $ \html -> do
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ (,,)
|
(result, widget) <- flip (renderAForm FormStandard) html $ (,,)
|
||||||
<$> areq (minIntField n "Zahl") (fromString $ "Ganzzahl > " ++ show n) Nothing
|
<$> areq (minIntFieldI n ("Zahl" :: Text)) (fromString $ "Ganzzahl > " ++ show n) Nothing
|
||||||
<* aformSection MsgFormBehaviour
|
<* aformSection MsgFormBehaviour
|
||||||
<*> areq checkBoxField "Muss nächste Zahl größer sein?" (Just True)
|
<*> areq checkBoxField "Muss nächste Zahl größer sein?" (Just True)
|
||||||
<*> areq doubleField "Fliesskommazahl" Nothing
|
<*> areq doubleField "Fliesskommazahl" Nothing
|
||||||
@ -194,7 +194,7 @@ postAdminTestR = do
|
|||||||
siteLayout locallyDefinedPageHeading $ do
|
siteLayout locallyDefinedPageHeading $ do
|
||||||
-- defaultLayout $ do
|
-- defaultLayout $ do
|
||||||
setTitle "Uni2work Admin Testpage"
|
setTitle "Uni2work Admin Testpage"
|
||||||
$(widgetFile "adminTest")
|
$(i18nWidgetFile "admin-test")
|
||||||
|
|
||||||
[whamlet|<h2>Formular Demonstration|]
|
[whamlet|<h2>Formular Demonstration|]
|
||||||
wrapForm formWidget FormSettings
|
wrapForm formWidget FormSettings
|
||||||
|
|||||||
@ -52,7 +52,7 @@ data ApplicationForm = ApplicationForm
|
|||||||
{ afPriority :: Maybe Natural
|
{ afPriority :: Maybe Natural
|
||||||
, afField :: Maybe StudyFeaturesId
|
, afField :: Maybe StudyFeaturesId
|
||||||
, afText :: Maybe Text
|
, afText :: Maybe Text
|
||||||
, afFiles :: Maybe (ConduitT () File Handler ())
|
, afFiles :: Maybe FileUploads
|
||||||
, afRatingVeto :: Bool
|
, afRatingVeto :: Bool
|
||||||
, afRatingPoints :: Maybe ExamGrade
|
, afRatingPoints :: Maybe ExamGrade
|
||||||
, afRatingComment :: Maybe Text
|
, afRatingComment :: Maybe Text
|
||||||
@ -291,8 +291,9 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
|
|||||||
, courseApplicationRatingTime = guardOn rated now
|
, courseApplicationRatingTime = guardOn rated now
|
||||||
}
|
}
|
||||||
let
|
let
|
||||||
sinkFile' file = do
|
sinkFile' (Right file) =
|
||||||
fId <- insert file
|
insert file >>= sinkFile' . Left
|
||||||
|
sinkFile' (Left fId) =
|
||||||
insert_ $ CourseApplicationFile appId fId
|
insert_ $ CourseApplicationFile appId fId
|
||||||
forM_ afFiles $ \afFiles' ->
|
forM_ afFiles $ \afFiles' ->
|
||||||
runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
|
runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
|
||||||
@ -308,7 +309,7 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
|
|||||||
| afmApplicantEdit afMode -> do
|
| afmApplicantEdit afMode -> do
|
||||||
oldFiles <- Set.fromList . map (courseApplicationFileFile . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] []
|
oldFiles <- Set.fromList . map (courseApplicationFileFile . entityVal) <$> selectList [CourseApplicationFileApplication ==. appId] []
|
||||||
changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' ->
|
changes <- flip execStateT oldFiles . forM_ afFiles $ \afFiles' ->
|
||||||
let sinkFile' file = do
|
let sinkFile' (Right file) = do
|
||||||
oldFiles' <- lift . E.select . E.from $ \(courseApplicationFile `E.InnerJoin` file') -> do
|
oldFiles' <- lift . E.select . E.from $ \(courseApplicationFile `E.InnerJoin` file') -> do
|
||||||
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file' E.^. FileId
|
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file' E.^. FileId
|
||||||
E.where_ $ file' E.^. FileTitle E.==. E.val (fileTitle file)
|
E.where_ $ file' E.^. FileTitle E.==. E.val (fileTitle file)
|
||||||
@ -326,7 +327,12 @@ editApplicationR maId uid cid mAppId afMode allowAction postAction = do
|
|||||||
fId <- lift $ insert file
|
fId <- lift $ insert file
|
||||||
lift . insert_ $ CourseApplicationFile appId fId
|
lift . insert_ $ CourseApplicationFile appId fId
|
||||||
modify $ Set.insert fId
|
modify $ Set.insert fId
|
||||||
in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
|
sinkFile' (Left fId)
|
||||||
|
| fId `Set.member` oldFiles = modify $ Set.delete fId
|
||||||
|
| otherwise = do
|
||||||
|
lift . insert_ $ CourseApplicationFile appId fId
|
||||||
|
modify $ Set.insert fId
|
||||||
|
in runConduit $ transPipe liftHandler afFiles' .| C.mapM_ sinkFile'
|
||||||
deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ]
|
deleteCascadeWhere [ FileId <-. Set.toList (oldFiles `Set.intersection` changes) ]
|
||||||
return changes
|
return changes
|
||||||
| otherwise
|
| otherwise
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
-- | Common handler functions.
|
-- | Common handler functions.
|
||||||
module Handler.Common where
|
module Handler.Common
|
||||||
|
( getFaviconR
|
||||||
|
, getRobotsR
|
||||||
|
) where
|
||||||
|
|
||||||
import Data.FileEmbed (embedFile)
|
import Data.FileEmbed (embedFile)
|
||||||
import Import hiding (embedFile)
|
import Import hiding (embedFile)
|
||||||
|
|||||||
@ -57,6 +57,8 @@ import qualified Control.Monad.State.Class as State
|
|||||||
|
|
||||||
import Data.Foldable (foldrM)
|
import Data.Foldable (foldrM)
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type CorrectionTableExpr = (E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
type CorrectionTableExpr = (E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity Sheet) `E.InnerJoin` E.SqlExpr (Entity Submission)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity User))
|
||||||
@ -215,6 +217,9 @@ colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ for
|
|||||||
_other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints)
|
_other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
colMaxPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData)))
|
||||||
|
colMaxPointsField = sortable (Just "sheet-type") (i18nCell MsgSheetType) $ i18nCell . (\DBRow{ dbrOutput=(_, Entity _ Sheet{sheetType}, _, _, _, _) } -> sheetType)
|
||||||
|
|
||||||
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
|
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
|
||||||
colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id
|
colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id
|
||||||
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId)
|
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _) } -> return subId)
|
||||||
@ -274,6 +279,13 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
, ( "rating"
|
, ( "rating"
|
||||||
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingPoints
|
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> submission E.^. SubmissionRatingPoints
|
||||||
)
|
)
|
||||||
|
, ( "sheet-type"
|
||||||
|
, SortColumns $ \((_ `E.InnerJoin` sheet `E.InnerJoin` _) `E.LeftOuterJoin` _) ->
|
||||||
|
[ SomeExprValue ((sheet E.^. SheetType) E.->. "type" :: E.SqlExpr (E.Value Value))
|
||||||
|
, SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "max" :: E.SqlExpr (E.Value Value))
|
||||||
|
, SomeExprValue (((sheet E.^. SheetType) E.->. "grading" :: E.SqlExpr (E.Value Value)) E.->. "passing" :: E.SqlExpr (E.Value Value))
|
||||||
|
]
|
||||||
|
)
|
||||||
, ( "israted"
|
, ( "israted"
|
||||||
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> E.not_ . E.isNothing $ submission E.^. SubmissionRatingTime
|
, SortColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _) -> E.not_ . E.isNothing $ submission E.^. SubmissionRatingTime
|
||||||
)
|
)
|
||||||
@ -367,11 +379,22 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
E.where_ $ (\f -> f user $ Set.singleton needle) $
|
E.where_ $ (\f -> f user $ Set.singleton needle) $
|
||||||
E.mkContainsFilter (E.^. UserMatrikelnummer)
|
E.mkContainsFilter (E.^. UserMatrikelnummer)
|
||||||
)
|
)
|
||||||
-- , ( "pseudonym"
|
, ( "rating-visible"
|
||||||
-- , FilterColumn $ E.mkExistsFilter $ \table needle -> E.from $ \(pseudonym) -> do
|
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) criterion -> case getLast (criterion :: Last Bool) of
|
||||||
-- E.where_ $ querySheet table E.^. SheetId E.==. pseudonym E.^. SheetPseudonymSheet
|
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
-- E.where_ $ E.mkContainsFilter -- DB only stores Pseudonym == Word24. Conversion not possible in DB.
|
Just True -> E.isJust $ submission E.^. SubmissionRatingTime
|
||||||
-- )
|
Just False-> E.isNothing $ submission E.^. SubmissionRatingTime
|
||||||
|
)
|
||||||
|
, ( "rating"
|
||||||
|
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) pts -> if
|
||||||
|
| Set.null pts -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
| otherwise -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (\p -> p `E.in_` E.valList (Set.toList pts)) (submission E.^. SubmissionRatingPoints)
|
||||||
|
)
|
||||||
|
, ( "comment"
|
||||||
|
, FilterColumn $ \((_ `E.InnerJoin` _ `E.InnerJoin` submission) `E.LeftOuterJoin` _ :: CorrectionTableExpr) comm -> case getLast (comm :: Last Text) of
|
||||||
|
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
|
Just needle -> E.maybe (E.val False :: E.SqlExpr (E.Value Bool)) (E.isInfixOf $ E.val needle) (submission E.^. SubmissionRatingComment)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
, dbtFilterUI = fromMaybe mempty dbtFilterUI
|
, dbtFilterUI = fromMaybe mempty dbtFilterUI
|
||||||
, dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI }
|
, dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI }
|
||||||
@ -605,7 +628,7 @@ postCorrectionsR = do
|
|||||||
, prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm)
|
, prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm)
|
||||||
, prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool)
|
, prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool)
|
||||||
, Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
, Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
||||||
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime)
|
||||||
]
|
]
|
||||||
courseOptions = runDB $ do
|
courseOptions = runDB $ do
|
||||||
courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
|
courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
|
||||||
@ -650,8 +673,8 @@ postCCorrectionsR tid ssh csh = do
|
|||||||
-- "pseudonym" TODO DB only stores Word24
|
-- "pseudonym" TODO DB only stores Word24
|
||||||
, Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
, Map.singleton "sheet-search" . maybeToList <$> aopt textField (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
||||||
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
||||||
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector)
|
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector)
|
||||||
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime)
|
||||||
]
|
]
|
||||||
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
||||||
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
correctionsR whereClause colonnade filterUI psValidator $ Map.fromList
|
||||||
@ -681,8 +704,8 @@ postSSubsR tid ssh csh shn = do
|
|||||||
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
[ prismAForm (singletonFilter "user-name-email") mPrev $ aopt textField (fslI MsgCourseMembers)
|
||||||
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
, prismAForm (singletonFilter "user-matriclenumber") mPrev $ aopt textField (fslI MsgMatrikelNr)
|
||||||
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
, prismAForm (singletonFilter "corrector-name-email") mPrev $ aopt textField (fslI MsgCorrector)
|
||||||
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgHasCorrector)
|
, prismAForm (singletonFilter "isassigned" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgHasCorrector)
|
||||||
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgRatingTime)
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime)
|
||||||
-- "pseudonym" TODO DB only stores Word24
|
-- "pseudonym" TODO DB only stores Word24
|
||||||
]
|
]
|
||||||
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
psValidator = def & defaultPagesize PagesizeAll -- Assisstant always want to see them all at once anyway
|
||||||
@ -714,13 +737,14 @@ postCorrectionR tid ssh csh shn cid = do
|
|||||||
|
|
||||||
results <- runDB $ correctionData tid ssh csh shn sub
|
results <- runDB $ correctionData tid ssh csh shn sub
|
||||||
|
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
case results of
|
case results of
|
||||||
[(Entity _ Course{..}, Entity _ Sheet{..}, Entity _ subm@Submission{..}, corrector)] -> do
|
[(Entity _ Course{..}, Entity _ Sheet{..}, Entity _ subm@Submission{..}, corrector)] -> do
|
||||||
let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c))
|
let ratingComment = fmap Text.strip submissionRatingComment >>= (\c -> c <$ guard (not $ null c))
|
||||||
pointsForm = case sheetType of
|
pointsForm = case sheetType of
|
||||||
NotGraded -> pure Nothing
|
NotGraded -> pure Nothing
|
||||||
_otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType)
|
_otherwise -> aopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType)
|
||||||
(fslpI MsgRatingPoints "Punktezahl" & setTooltip sheetType)
|
(fslpI MsgRatingPoints (mr MsgPointsPlaceholder) & setTooltip sheetType)
|
||||||
(Just submissionRatingPoints)
|
(Just submissionRatingPoints)
|
||||||
|
|
||||||
((corrResult, corrForm'), corrEncoding) <- runFormPost . identifyForm FIDcorrection . renderAForm FormStandard $ (,,)
|
((corrResult, corrForm'), corrEncoding) <- runFormPost . identifyForm FIDcorrection . renderAForm FormStandard $ (,,)
|
||||||
@ -769,14 +793,13 @@ postCorrectionR tid ssh csh shn cid = do
|
|||||||
formResult uploadResult $ \fileUploads -> do
|
formResult uploadResult $ \fileUploads -> do
|
||||||
uid <- requireAuthId
|
uid <- requireAuthId
|
||||||
|
|
||||||
res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
|
res <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) fileUploads .| C.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission uid (Right sub) True
|
||||||
case res of
|
case res of
|
||||||
Nothing -> return () -- ErrorMessages are already added by msgSubmissionErrors
|
Nothing -> return () -- ErrorMessages are already added by msgSubmissionErrors
|
||||||
(Just _) -> do
|
(Just _) -> do
|
||||||
addMessageI Success MsgRatingFilesUpdated
|
addMessageI Success MsgRatingFilesUpdated
|
||||||
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
redirect $ CSubmissionR tid ssh csh shn cid CorrectionR
|
||||||
|
|
||||||
mr <- getMessageRender
|
|
||||||
let sheetTypeDesc = mr sheetType
|
let sheetTypeDesc = mr sheetType
|
||||||
heading = MsgCorrectionHead tid ssh csh shn cid
|
heading = MsgCorrectionHead tid ssh csh shn cid
|
||||||
headingWgt = [whamlet|
|
headingWgt = [whamlet|
|
||||||
@ -818,7 +841,7 @@ postCorrectionsUploadR = do
|
|||||||
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
||||||
FormSuccess files -> do
|
FormSuccess files -> do
|
||||||
uid <- requireAuthId
|
uid <- requireAuthId
|
||||||
mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) files .| extractRatingsMsg .| sinkMultiSubmission uid True
|
mbSubs <- msgSubmissionErrors . runDBJobs . runConduit $ transPipe (lift . lift) files .| C.mapM (either get404 return) .| extractRatingsMsg .| sinkMultiSubmission uid True
|
||||||
case mbSubs of
|
case mbSubs of
|
||||||
Nothing -> return ()
|
Nothing -> return ()
|
||||||
(Just subs)
|
(Just subs)
|
||||||
@ -868,9 +891,10 @@ postCorrectionsCreateR = do
|
|||||||
, optionInternalValue = sid
|
, optionInternalValue = sid
|
||||||
, optionExternalValue = toPathPiece (cID :: CryptoUUIDSheet)
|
, optionExternalValue = toPathPiece (cID :: CryptoUUIDSheet)
|
||||||
}
|
}
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
((pseudonymRes, pseudonymWidget), pseudonymEncoding) <- runFormPost . renderAForm FormStandard $ (,)
|
((pseudonymRes, pseudonymWidget), pseudonymEncoding) <- runFormPost . renderAForm FormStandard $ (,)
|
||||||
<$> areq (selectField sheetOptions) (fslI MsgPseudonymSheet) Nothing
|
<$> areq (selectField sheetOptions) (fslI MsgPseudonymSheet) Nothing
|
||||||
<*> (textToList <$> areq textareaField (fslpI MsgCorrectionPseudonyms "Pseudonyme" & setTooltip MsgCorrectionPseudonymsTip) Nothing)
|
<*> (textToList <$> areq textareaField (fslpI MsgCorrectionPseudonyms (mr MsgPseudonyms) & setTooltip MsgCorrectionPseudonymsTip) Nothing)
|
||||||
|
|
||||||
case pseudonymRes of
|
case pseudonymRes of
|
||||||
FormMissing -> return ()
|
FormMissing -> return ()
|
||||||
@ -980,7 +1004,8 @@ postCorrectionsCreateR = do
|
|||||||
, formEncoding = pseudonymEncoding
|
, formEncoding = pseudonymEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultLayout
|
siteLayoutMsg MsgCorrCreate $ do
|
||||||
|
setTitleI MsgCorrCreate
|
||||||
$(widgetFile "corrections-create")
|
$(widgetFile "corrections-create")
|
||||||
where
|
where
|
||||||
partitionEithers' :: [[Either a b]] -> ([[b]], [a])
|
partitionEithers' :: [[Either a b]] -> ([[b]], [a])
|
||||||
@ -1010,8 +1035,28 @@ postCorrectionsGradeR = do
|
|||||||
, colRated
|
, colRated
|
||||||
, colRatedField
|
, colRatedField
|
||||||
, colPointsField
|
, colPointsField
|
||||||
|
, colMaxPointsField
|
||||||
, colCommentField
|
, colCommentField
|
||||||
] -- Continue here
|
] -- Continue here
|
||||||
|
filterUI = Just $ \mPrev -> mconcat
|
||||||
|
[ prismAForm (singletonFilter "course" ) mPrev $ aopt (lift `hoistField` selectField courseOptions) (fslI MsgCourse)
|
||||||
|
, prismAForm (singletonFilter "term" ) mPrev $ aopt (lift `hoistField` selectField termOptions) (fslI MsgTerm)
|
||||||
|
, prismAForm (singletonFilter "school" ) mPrev $ aopt (lift `hoistField` selectField schoolOptions) (fslI MsgCourseSchool)
|
||||||
|
, Map.singleton "sheet-search" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgSheet) (Just <$> listToMaybe =<< ((Map.lookup "sheet-search" =<< mPrev) <|> (Map.lookup "sheet" =<< mPrev)))
|
||||||
|
, prismAForm (singletonFilter "israted" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingTime)
|
||||||
|
, prismAForm (singletonFilter "rating-visible" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgRatingDone)
|
||||||
|
, prismAForm (singletonFilter "rating" . maybePrism _PathPiece) mPrev $ aopt (lift `hoistField` pointsField) (fslI MsgColumnRatingPoints)
|
||||||
|
, Map.singleton "comment" . maybeToList <$> aopt (lift `hoistField` textField) (fslI MsgRatingComment) (Just <$> listToMaybe =<< (Map.lookup "comment" =<< mPrev))
|
||||||
|
]
|
||||||
|
courseOptions = runDB $ do
|
||||||
|
courses <- selectList [] [Asc CourseShorthand] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
|
||||||
|
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . courseShorthand . entityVal) courses
|
||||||
|
termOptions = runDB $ do
|
||||||
|
courses <- selectList [] [Asc CourseTerm] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
|
||||||
|
optionsPairs $ map (id &&& id) $ nub $ map (termToText . unTermKey . courseTerm . entityVal) courses
|
||||||
|
schoolOptions = runDB $ do
|
||||||
|
courses <- selectList [] [Asc CourseSchool] >>= filterM (\(Entity _ Course{..}) -> (== Authorized) <$> evalAccessCorrector courseTerm courseSchool courseShorthand)
|
||||||
|
optionsPairs $ map (id &&& id) $ nub $ map (CI.original . unSchoolKey . courseSchool . entityVal) courses
|
||||||
psValidator = def
|
psValidator = def
|
||||||
& defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData))
|
& defaultSorting [SortDescBy "ratingtime"] :: PSValidator (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult SubmissionId (Bool, Maybe Points, Maybe Text) CorrectionTableData))
|
||||||
unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment)
|
unFormResult = getDBFormResult $ \DBRow{ dbrOutput = (Entity _ sub@Submission{..}, _, _, _, _, _) } -> (submissionRatingDone sub, submissionRatingPoints, submissionRatingComment)
|
||||||
@ -1020,7 +1065,7 @@ postCorrectionsGradeR = do
|
|||||||
void . assertM (== Authorized) . lift $ evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) True
|
void . assertM (== Authorized) . lift $ evalAccessDB (CSubmissionR tid ssh csh shn cID CorrectionR) True
|
||||||
return i
|
return i
|
||||||
|
|
||||||
(fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns mempty psValidator dbtProj' $ def
|
(fmap unFormResult -> tableRes, table) <- runDB $ makeCorrectionsTable whereClause displayColumns filterUI psValidator dbtProj' $ def
|
||||||
{ dbParamsFormAction = Just $ SomeRoute CorrectionsGradeR
|
{ dbParamsFormAction = Just $ SomeRoute CorrectionsGradeR
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1045,7 +1090,8 @@ postCorrectionsGradeR = do
|
|||||||
content = Right $(widgetFile "messages/correctionsUploaded")
|
content = Right $(widgetFile "messages/correctionsUploaded")
|
||||||
unless (null subs') $ addMessageModal Success trigger content
|
unless (null subs') $ addMessageModal Success trigger content
|
||||||
|
|
||||||
defaultLayout $
|
siteLayoutMsg MsgCorrectionsGrade $ do
|
||||||
|
setTitleI MsgCorrectionsGrade
|
||||||
$(widgetFile "corrections-grade")
|
$(widgetFile "corrections-grade")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -256,7 +256,6 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
|
|
||||||
optionalActionW' (bool mforcedJust mpopt mayChange) allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template)
|
optionalActionW' (bool mforcedJust mpopt mayChange) allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template)
|
||||||
|
|
||||||
-- TODO: internationalization
|
|
||||||
-- let autoUnzipInfo = [|Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu. TODO|]
|
-- let autoUnzipInfo = [|Entpackt hochgeladene Zip-Dateien (*.zip) automatisch und fügt den Inhalt dem Stamm-Verzeichnis der Abgabe hinzu. TODO|]
|
||||||
|
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
|
(result, widget) <- flip (renderAForm FormStandard) html $ CourseForm
|
||||||
@ -267,9 +266,9 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
& setTooltip MsgCourseShorthandUnique) (cfShort <$> template)
|
& setTooltip MsgCourseShorthandUnique) (cfShort <$> template)
|
||||||
<*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template)
|
<*> areq (schoolFieldFor userSchools) (fslI MsgCourseSchool) (cfSchool <$> template)
|
||||||
<*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template)
|
<*> areq termsField (fslI MsgCourseSemester) (cfTerm <$> template)
|
||||||
<*> aopt htmlField (fslpI MsgCourseDescription "Bitte mindestens die Modulbeschreibung angeben"
|
<*> aopt htmlField (fslpI MsgCourseDescription (mr MsgCourseDescriptionPlaceholder)
|
||||||
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
|
& setTooltip MsgCourseDescriptionTip) (cfDesc <$> template)
|
||||||
<*> aopt (urlField & cfStrip) (fslpI MsgCourseHomepageExternal "Optionale externe URL")
|
<*> aopt (urlField & cfStrip) (fslpI MsgCourseHomepageExternal (mr MsgCourseHomepageExternalPlaceholder))
|
||||||
(cfLink <$> template)
|
(cfLink <$> template)
|
||||||
<*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
|
<*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
|
||||||
<* aformSection MsgCourseFormSectionRegistration
|
<* aformSection MsgCourseFormSectionRegistration
|
||||||
|
|||||||
@ -66,7 +66,7 @@ lecturerInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseLecInviteHeading $ CI.original courseName
|
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseLecInviteHeading $ CI.original courseName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseLecInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseLecInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ (InvDBDataLecturer mlType, _) _ = hoistAForm liftHandler $ toJunction <$> case mlType of
|
invitationForm _ (InvDBDataLecturer mlType, _) _ = hoistAForm liftHandler $ toJunction <$> case mlType of
|
||||||
|
|||||||
@ -176,7 +176,7 @@ makeCourseTable whereClause colChoices psValidator = do
|
|||||||
, Just $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer)
|
, Just $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer)
|
||||||
, Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch)
|
, Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch)
|
||||||
, Just $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgCourseRegisterOpen)
|
, Just $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgCourseRegisterOpen)
|
||||||
, muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt boolField (fslI MsgCourseFilterRegistered))
|
, muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseFilterRegistered))
|
||||||
]
|
]
|
||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
{ dbsFilterLayout = defaultDBSFilterLayout
|
{ dbsFilterLayout = defaultDBSFilterLayout
|
||||||
|
|||||||
@ -83,7 +83,7 @@ participantInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseParticipantInviteHeading $ CI.original courseName
|
invitationHeading (Entity _ Course{..}) _ = return . SomeMessage $ MsgCourseParticipantInviteHeading $ CI.original courseName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseParticipantInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgCourseParticipantInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do
|
invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do
|
||||||
|
|||||||
@ -42,7 +42,7 @@ instance Button UniWorX ButtonCourseRegister where
|
|||||||
data CourseRegisterForm = CourseRegisterForm
|
data CourseRegisterForm = CourseRegisterForm
|
||||||
{ crfStudyFeatures :: Maybe StudyFeaturesId
|
{ crfStudyFeatures :: Maybe StudyFeaturesId
|
||||||
, crfApplicationText :: Maybe Text
|
, crfApplicationText :: Maybe Text
|
||||||
, crfApplicationFiles :: Maybe (ConduitT () File Handler ())
|
, crfApplicationFiles :: Maybe FileUploads
|
||||||
}
|
}
|
||||||
|
|
||||||
courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister)
|
courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister)
|
||||||
@ -195,7 +195,7 @@ postCRegisterR tid ssh csh = do
|
|||||||
whenIsJust appRes $
|
whenIsJust appRes $
|
||||||
audit . TransactionCourseApplicationEdit cid uid
|
audit . TransactionCourseApplicationEdit cid uid
|
||||||
whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do
|
whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do
|
||||||
runConduit $ transPipe liftHandler fSource .| C.mapM_ (\f -> insert f >>= insert_ . CourseApplicationFile appId)
|
runConduit $ transPipe liftHandler fSource .| C.mapM_ (insert_ . CourseApplicationFile appId <=< either return insert)
|
||||||
return appRes
|
return appRes
|
||||||
| otherwise
|
| otherwise
|
||||||
= return $ Just ()
|
= return $ Just ()
|
||||||
|
|||||||
@ -66,7 +66,7 @@ instance (CryptoRoute ciphertext plaintext, Dispatch ciphertext ps) => Dispatch
|
|||||||
|
|
||||||
|
|
||||||
getCryptoUUIDDispatchR :: UUID -> Handler ()
|
getCryptoUUIDDispatchR :: UUID -> Handler ()
|
||||||
getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith found302)
|
getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectAccessWith movedPermanently301)
|
||||||
where
|
where
|
||||||
p :: Proxy '[ SubmissionId
|
p :: Proxy '[ SubmissionId
|
||||||
, UserId
|
, UserId
|
||||||
@ -74,7 +74,7 @@ getCryptoUUIDDispatchR uuid = dispatchID p uuid >>= maybe notFound (redirectWith
|
|||||||
p = Proxy
|
p = Proxy
|
||||||
|
|
||||||
getCryptoFileNameDispatchR :: CI FilePath -> Handler ()
|
getCryptoFileNameDispatchR :: CI FilePath -> Handler ()
|
||||||
getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectWith found302)
|
getCryptoFileNameDispatchR path = dispatchID p path >>= maybe notFound (redirectAccessWith movedPermanently301)
|
||||||
where
|
where
|
||||||
p :: Proxy '[ SubmissionId ]
|
p :: Proxy '[ SubmissionId ]
|
||||||
p = Proxy
|
p = Proxy
|
||||||
|
|||||||
@ -67,7 +67,7 @@ examCorrectorInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName
|
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInviteHeading examName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamCorrectorInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionExamCorrector, ())
|
invitationForm _ _ _ = pure (JunctionExamCorrector, ())
|
||||||
|
|||||||
@ -77,7 +77,7 @@ examRegistrationInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamRegistrationInviteHeading examName
|
invitationHeading (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamRegistrationInviteHeading examName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamRegistrationInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamRegistrationInviteExplanation}|]
|
||||||
invitationTokenConfig _ (InvDBDataExamRegistration{..}, _) = do
|
invitationTokenConfig _ (InvDBDataExamRegistration{..}, _) = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
let itExpiresAt = Just $ Just invDBExamRegistrationDeadline
|
let itExpiresAt = Just $ Just invDBExamRegistrationDeadline
|
||||||
itAddAuth
|
itAddAuth
|
||||||
| not invDBExamRegistrationCourseRegister
|
| not invDBExamRegistrationCourseRegister
|
||||||
|
|||||||
@ -369,7 +369,7 @@ postEGradesR tid ssh csh examn = do
|
|||||||
, fltrStudyDegreeUI
|
, fltrStudyDegreeUI
|
||||||
, fltrStudyFeaturesSemesterUI
|
, fltrStudyFeaturesSemesterUI
|
||||||
, fltrExamResultPointsUI examShowGrades
|
, fltrExamResultPointsUI examShowGrades
|
||||||
, \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgExamUserSynchronised)
|
, \mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised)
|
||||||
]
|
]
|
||||||
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
dbtParams = DBParamsForm
|
dbtParams = DBParamsForm
|
||||||
|
|||||||
@ -67,7 +67,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
|
|||||||
return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName
|
return . SomeMessage $ MsgExamOfficeUserInviteHeading userDisplayName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExamOfficeUserInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
let itExpiresAt = Nothing
|
let itExpiresAt = Nothing
|
||||||
itStartsAt = Nothing
|
itStartsAt = Nothing
|
||||||
itAddAuth = Nothing
|
itAddAuth = Nothing
|
||||||
|
|||||||
@ -32,7 +32,7 @@ homeUpcomingSheets uid = do
|
|||||||
, E.SqlExpr (E.Value SchoolId)
|
, E.SqlExpr (E.Value SchoolId)
|
||||||
, E.SqlExpr (E.Value CourseShorthand)
|
, E.SqlExpr (E.Value CourseShorthand)
|
||||||
, E.SqlExpr (E.Value SheetName)
|
, E.SqlExpr (E.Value SheetName)
|
||||||
, E.SqlExpr (E.Value UTCTime)
|
, E.SqlExpr (E.Value (Maybe UTCTime))
|
||||||
, E.SqlExpr (E.Value (Maybe SubmissionId)))
|
, E.SqlExpr (E.Value (Maybe SubmissionId)))
|
||||||
tableData ((participant `E.InnerJoin` course `E.InnerJoin` sheet) `E.LeftOuterJoin` (submission `E.InnerJoin` subuser)) = do
|
tableData ((participant `E.InnerJoin` course `E.InnerJoin` sheet) `E.LeftOuterJoin` (submission `E.InnerJoin` subuser)) = do
|
||||||
E.on $ submission E.?. SubmissionId E.==. subuser E.?. SubmissionUserSubmission
|
E.on $ submission E.?. SubmissionId E.==. subuser E.?. SubmissionUserSubmission
|
||||||
@ -41,7 +41,7 @@ homeUpcomingSheets uid = do
|
|||||||
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
||||||
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
||||||
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
|
E.where_ $ participant E.^. CourseParticipantUser E.==. E.val uid
|
||||||
E.&&. sheet E.^. SheetActiveTo E.>=. E.val cTime
|
E.&&. E.maybe E.true (E.>=. E.val cTime) (sheet E.^. SheetActiveTo)
|
||||||
return
|
return
|
||||||
( course E.^. CourseTerm
|
( course E.^. CourseTerm
|
||||||
, course E.^. CourseSchool
|
, course E.^. CourseSchool
|
||||||
@ -55,7 +55,7 @@ homeUpcomingSheets uid = do
|
|||||||
, E.Value SchoolId
|
, E.Value SchoolId
|
||||||
, E.Value CourseShorthand
|
, E.Value CourseShorthand
|
||||||
, E.Value SheetName
|
, E.Value SheetName
|
||||||
, E.Value UTCTime
|
, E.Value (Maybe UTCTime)
|
||||||
, E.Value (Maybe SubmissionId)
|
, E.Value (Maybe SubmissionId)
|
||||||
))
|
))
|
||||||
(DBCell Handler ())
|
(DBCell Handler ())
|
||||||
@ -70,8 +70,8 @@ homeUpcomingSheets uid = do
|
|||||||
anchorCell (CourseR tid ssh csh CShowR) csh
|
anchorCell (CourseR tid ssh csh CShowR) csh
|
||||||
, sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } ->
|
, sortable (Just "sheet") (i18nCell MsgSheet) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, _) } ->
|
||||||
anchorCell (CSheetR tid ssh csh shn SShowR) shn
|
anchorCell (CSheetR tid ssh csh shn SShowR) shn
|
||||||
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value deadline, _) } ->
|
, sortable (Just "deadline") (i18nCell MsgDeadline) $ \DBRow{ dbrOutput=(_, _, _, _, E.Value mDeadline, _) } ->
|
||||||
cell $ formatTime SelFormatDateTime deadline >>= toWidget
|
maybe mempty (cell . formatTimeW SelFormatDateTime) mDeadline
|
||||||
, sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } ->
|
, sortable (Just "done") (i18nCell MsgDone) $ \DBRow{ dbrOutput=(E.Value tid, E.Value ssh, E.Value csh, E.Value shn, _, E.Value mbsid) } ->
|
||||||
case mbsid of
|
case mbsid of
|
||||||
Nothing -> cell $ do
|
Nothing -> cell $ do
|
||||||
|
|||||||
@ -2,6 +2,10 @@ module Handler.Info where
|
|||||||
|
|
||||||
import Import
|
import Import
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
import Handler.Info.TH
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import Development.GitRev
|
import Development.GitRev
|
||||||
|
|
||||||
@ -67,3 +71,18 @@ getInfoLecturerR =
|
|||||||
if currentTime > expiryTime
|
if currentTime > expiryTime
|
||||||
then mempty
|
then mempty
|
||||||
else toWidget [whamlet| ^{iconTooltip tooltipNew (Just IconNew) False} |]
|
else toWidget [whamlet| ^{iconTooltip tooltipNew (Just IconNew) False} |]
|
||||||
|
|
||||||
|
getGlossaryR :: Handler Html
|
||||||
|
getGlossaryR =
|
||||||
|
siteLayoutMsg' MsgGlossaryTitle $ do
|
||||||
|
setTitleI MsgGlossaryTitle
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
let
|
||||||
|
entries' = sortOn (CI.mk . view _2) $ do
|
||||||
|
(k, v) <- Map.toList entries
|
||||||
|
msg <- maybeToList $ Map.lookup k msgMap
|
||||||
|
return (k, mr msg, v)
|
||||||
|
$(widgetFile "glossary")
|
||||||
|
where
|
||||||
|
entries = $(i18nWidgetFiles "glossary")
|
||||||
|
msgMap = $(glossaryTerms "glossary")
|
||||||
|
|||||||
23
src/Handler/Info/TH.hs
Normal file
23
src/Handler/Info/TH.hs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
module Handler.Info.TH
|
||||||
|
( glossaryTerms
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils.I18n
|
||||||
|
|
||||||
|
import Language.Haskell.TH
|
||||||
|
|
||||||
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
|
||||||
|
glossaryTerms :: FilePath -> Q Exp
|
||||||
|
glossaryTerms basename = do
|
||||||
|
translationsAvailable <- i18nWidgetFilesAvailable' basename
|
||||||
|
let terms = Map.mapWithKey (\k _ -> "Msg" <> unPathPiece k) translationsAvailable
|
||||||
|
[e|Map.fromList $(listE . map (\(int, msg) -> tupE [litE . stringL $ repack int, conE $ mkName msg]) $ Map.toList terms)|]
|
||||||
|
where
|
||||||
|
unPathPiece :: Text -> String
|
||||||
|
unPathPiece = repack . mconcat . map (over _head Char.toUpper) . Text.splitOn "-"
|
||||||
45
src/Handler/Metrics.hs
Normal file
45
src/Handler/Metrics.hs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
module Handler.Metrics
|
||||||
|
( getMetricsR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import hiding (Info)
|
||||||
|
|
||||||
|
import Prometheus
|
||||||
|
import qualified Network.Wai.Middleware.Prometheus as Prometheus
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.HashSet as HashSet
|
||||||
|
|
||||||
|
|
||||||
|
getMetricsR :: Handler TypedContent
|
||||||
|
getMetricsR = selectRep $ do
|
||||||
|
provideRep (sendWaiApplication Prometheus.metricsApp :: Handler Text)
|
||||||
|
provideRep metricsHtml
|
||||||
|
provideRep $ collectMetrics >>= returnJson
|
||||||
|
where
|
||||||
|
metricsHtml :: Handler Html
|
||||||
|
metricsHtml = do
|
||||||
|
samples <- collectMetrics
|
||||||
|
|
||||||
|
metricsToken <- runMaybeT . hoist runDB $ do
|
||||||
|
uid <- MaybeT maybeAuthId
|
||||||
|
guardM . lift . existsBy $ UniqueUserGroupMember UserGroupMetrics uid
|
||||||
|
|
||||||
|
encodeToken =<< bearerToken (Left $ toJSON UserGroupMetrics) (Just $ HashSet.singleton MetricsR) Nothing (Just Nothing) Nothing
|
||||||
|
|
||||||
|
defaultLayout $ do
|
||||||
|
setTitleI MsgTitleMetrics
|
||||||
|
$(widgetFile "metrics")
|
||||||
|
|
||||||
|
metricBasename base sName
|
||||||
|
| Just suffix <- Text.stripPrefix base sName
|
||||||
|
= if | Just suffix' <- Text.stripPrefix "_" suffix
|
||||||
|
-> suffix'
|
||||||
|
| otherwise
|
||||||
|
-> suffix
|
||||||
|
| otherwise
|
||||||
|
= sName
|
||||||
|
getLabels = nub . concatMap (\(Sample _ lPairs _) -> lPairs ^.. folded . _1)
|
||||||
|
singleSample base [Sample sName lPairs sValue]
|
||||||
|
| sName == base = Just (lPairs, sValue)
|
||||||
|
singleSample _ _ = Nothing
|
||||||
@ -5,6 +5,7 @@ module Handler.Profile
|
|||||||
, getUserNotificationR, postUserNotificationR
|
, getUserNotificationR, postUserNotificationR
|
||||||
, getSetDisplayEmailR, postSetDisplayEmailR
|
, getSetDisplayEmailR, postSetDisplayEmailR
|
||||||
, getCsvOptionsR, postCsvOptionsR
|
, getCsvOptionsR, postCsvOptionsR
|
||||||
|
, postLangR
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -22,6 +23,8 @@ import qualified Data.Set as Set
|
|||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
-- import Database.Esqueleto ((^.))
|
-- import Database.Esqueleto ((^.))
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import Data.List (inits)
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
@ -76,15 +79,16 @@ instance RenderMessage UniWorX NotificationTriggerKind where
|
|||||||
|
|
||||||
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
|
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
|
||||||
makeSettingForm template html = do
|
makeSettingForm template html = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
|
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
|
||||||
<$ aformSection MsgFormPersonalAppearance
|
<$ aformSection MsgFormPersonalAppearance
|
||||||
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
|
<*> areq (textField & cfStrip) (fslI MsgUserDisplayName & setTooltip MsgUserDisplayNameRulesBelow) (stgDisplayName <$> template)
|
||||||
<*> areq (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
|
<*> areq (emailField & cfStrip & cfCI) (fslI MsgUserDisplayEmail & setTooltip MsgUserDisplayEmailTip) (stgDisplayEmail <$> template)
|
||||||
<* aformSection MsgFormCosmetics
|
<* aformSection MsgFormCosmetics
|
||||||
<*> areq (natFieldI $ MsgNatField "Favoriten")
|
<*> areq (natFieldI MsgFavouritesNotNatural)
|
||||||
(fslpI MsgFavourites "Anzahl Favoriten" & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
|
(fslpI MsgFavourites (mr MsgFavouritesPlaceholder) & setTooltip MsgFavouritesTip) (stgMaxFavourites <$> template)
|
||||||
<*> areq (natFieldI $ MsgNatField "Favoriten-Semester")
|
<*> areq (natFieldI MsgFavouritesSemestersNotNatural)
|
||||||
(fslpI MsgFavouriteSemesters "Anzahl Semester") (stgMaxFavouriteTerms <$> template)
|
(fslpI MsgFavouriteSemesters (mr MsgFavouritesSemestersPlaceholder)) (stgMaxFavouriteTerms <$> template)
|
||||||
<*> areq (selectField . return $ mkOptionList themeList)
|
<*> areq (selectField . return $ mkOptionList themeList)
|
||||||
(fslI MsgTheme) { fsId = Just "theme-select" } (stgTheme <$> template)
|
(fslI MsgTheme) { fsId = Just "theme-select" } (stgTheme <$> template)
|
||||||
<*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template)
|
<*> areq (selectField $ dateTimeFormatOptions SelFormatDateTime) (fslI MsgDateTimeFormat) (stgDateTime <$> template)
|
||||||
@ -315,7 +319,7 @@ postProfileR = do
|
|||||||
tResetTime <- traverse (formatTime SelFormatDateTime) userTokensIssuedAfter
|
tResetTime <- traverse (formatTime SelFormatDateTime) userTokensIssuedAfter
|
||||||
|
|
||||||
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
siteLayout [whamlet|_{MsgProfileFor} ^{nameWidget userDisplayName userSurname}|] $ do
|
||||||
setTitle . toHtml $ "Profil " <> userIdent
|
setTitleI MsgProfileTitle
|
||||||
let settingsForm =
|
let settingsForm =
|
||||||
wrapForm formWidget FormSettings
|
wrapForm formWidget FormSettings
|
||||||
{ formMethod = POST
|
{ formMethod = POST
|
||||||
@ -366,10 +370,11 @@ makeProfileData (Entity uid User{..}) = do
|
|||||||
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
|
||||||
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
|
||||||
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
|
||||||
let examTable = [whamlet|Prüfungen werden hier momentan leider noch nicht unterstützt.|]
|
let examTable = [whamlet|_{MsgPersonalInfoExamAchievementsWip}|]
|
||||||
let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
let ownTutorialTable = [whamlet|_{MsgPersonalInfoOwnTutorialsWip}|]
|
||||||
let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
|
let tutorialTable = [whamlet|_{MsgPersonalInfoTutorialsWip}|]
|
||||||
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication
|
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication
|
||||||
|
let profileRemarks = $(i18nWidgetFile "profile-remarks")
|
||||||
return $(widgetFile "profileData")
|
return $(widgetFile "profileData")
|
||||||
|
|
||||||
|
|
||||||
@ -837,3 +842,18 @@ postCsvOptionsR = do
|
|||||||
, formEncoding = optionsEnctype
|
, formEncoding = optionsEnctype
|
||||||
, formAttrs = [ asyncSubmitAttr | isModal ]
|
, formAttrs = [ asyncSubmitAttr | isModal ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postLangR :: Handler ()
|
||||||
|
postLangR = do
|
||||||
|
((langRes, _), _) <- runFormPost $ identifyForm FIDLanguage langForm
|
||||||
|
|
||||||
|
formResult langRes $ \(lang, route) -> do
|
||||||
|
lang' <- runDB . updateUserLanguage $ Just lang
|
||||||
|
|
||||||
|
app <- getYesod
|
||||||
|
let mr | Just lang'' <- lang' = renderMessage app . map (Text.intercalate "-") . reverse . inits $ Text.splitOn "-" lang''
|
||||||
|
| otherwise = renderMessage app []
|
||||||
|
addMessage Success . toHtml $ mr MsgLanguageChanged
|
||||||
|
redirect route
|
||||||
|
|
||||||
|
invalidArgs ["Language form required"]
|
||||||
|
|||||||
@ -55,6 +55,8 @@ import Text.Hamlet (ihamlet)
|
|||||||
|
|
||||||
import System.FilePath (addExtension)
|
import System.FilePath (addExtension)
|
||||||
|
|
||||||
|
import Data.Time.Clock.System (systemEpochDay)
|
||||||
|
|
||||||
|
|
||||||
{-
|
{-
|
||||||
* Implement Handlers
|
* Implement Handlers
|
||||||
@ -62,22 +64,38 @@ import System.FilePath (addExtension)
|
|||||||
* Implement Access in Foundation
|
* Implement Access in Foundation
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
type Loads = Map (Either UserEmail UserId) (InvitationData SheetCorrector)
|
||||||
|
|
||||||
data SheetForm = SheetForm
|
data SheetForm = SheetForm
|
||||||
{ sfName :: SheetName
|
{ sfName :: SheetName
|
||||||
|
, sfDescription :: Maybe Html
|
||||||
|
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ())
|
||||||
, sfVisibleFrom :: Maybe UTCTime
|
, sfVisibleFrom :: Maybe UTCTime
|
||||||
, sfActiveFrom :: UTCTime
|
, sfActiveFrom :: Maybe UTCTime
|
||||||
, sfActiveTo :: UTCTime
|
, sfActiveTo :: Maybe UTCTime
|
||||||
, sfHintFrom :: Maybe UTCTime
|
, sfHintFrom :: Maybe UTCTime
|
||||||
, sfSolutionFrom :: Maybe UTCTime
|
, sfSolutionFrom :: Maybe UTCTime
|
||||||
, sfSheetF, sfHintF, sfSolutionF, sfMarkingF :: Maybe (ConduitT () (Either FileId File) Handler ())
|
|
||||||
, sfType :: SheetType
|
, sfType :: SheetType
|
||||||
, sfGrouping :: SheetGroup
|
, sfGrouping :: SheetGroup
|
||||||
, sfSubmissionMode :: SubmissionMode
|
, sfSubmissionMode :: SubmissionMode
|
||||||
, sfDescription :: Maybe Html
|
, sfAutoDistribute :: Bool
|
||||||
, sfMarkingText :: Maybe Html
|
, sfMarkingText :: Maybe Html
|
||||||
|
, sfCorrectors :: Loads
|
||||||
-- Keine SheetId im Formular!
|
-- Keine SheetId im Formular!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data ButtonGeneratePseudonym = BtnGenerate
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ButtonGeneratePseudonym
|
||||||
|
instance Finite ButtonGeneratePseudonym
|
||||||
|
|
||||||
|
nullaryPathPiece ''ButtonGeneratePseudonym (camelToPathPiece' 1)
|
||||||
|
|
||||||
|
instance Button UniWorX ButtonGeneratePseudonym where
|
||||||
|
btnLabel BtnGenerate = [whamlet|_{MsgSheetGeneratePseudonym}|]
|
||||||
|
btnClasses BtnGenerate = [BCIsButton, BCDefault]
|
||||||
|
|
||||||
|
|
||||||
getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileId)
|
getFtIdMap :: Key Sheet -> DB (SheetFileType -> Set FileId)
|
||||||
getFtIdMap sId = do
|
getFtIdMap sId = do
|
||||||
allfIds <- E.select . E.from $ \(sheetFile `E.InnerJoin` file) -> do
|
allfIds <- E.select . E.from $ \(sheetFile `E.InnerJoin` file) -> do
|
||||||
@ -91,40 +109,41 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
|
|||||||
oldFileIds <- (return.) <$> case msId of
|
oldFileIds <- (return.) <$> case msId of
|
||||||
Nothing -> return $ partitionFileType mempty
|
Nothing -> return $ partitionFileType mempty
|
||||||
(Just sId) -> liftHandler $ runDB $ getFtIdMap sId
|
(Just sId) -> liftHandler $ runDB $ getFtIdMap sId
|
||||||
mr <- getMsgRenderer
|
mr'@(MsgRenderer mr) <- getMsgRenderer
|
||||||
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
|
ctime <- ceilingQuarterHour <$> liftIO getCurrentTime
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
|
(result, widget) <- flip (renderAForm FormStandard) html $ SheetForm
|
||||||
<$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template)
|
<$> areq (textField & cfStrip & cfCI) (fslI MsgSheetName) (sfName <$> template)
|
||||||
<* aformSection MsgSheetFormTimes
|
<*> aopt htmlField (fslpI MsgSheetDescription "Html") (sfDescription <$> template)
|
||||||
<*> aopt utcTimeField (fslI MsgSheetVisibleFrom
|
|
||||||
& setTooltip MsgSheetVisibleFromTip)
|
|
||||||
((sfVisibleFrom <$> template) <|> pure (Just ctime))
|
|
||||||
<*> areq utcTimeField (fslI MsgSheetActiveFrom
|
|
||||||
& setTooltip MsgSheetActiveFromTip)
|
|
||||||
(sfActiveFrom <$> template)
|
|
||||||
<*> areq utcTimeField (fslI MsgSheetActiveTo) (sfActiveTo <$> template)
|
|
||||||
<*> aopt utcTimeField (fslpI MsgSheetHintFrom "Datum, sonst nur für Korrektoren"
|
|
||||||
& setTooltip MsgSheetHintFromTip) (sfHintFrom <$> template)
|
|
||||||
<*> aopt utcTimeField (fslpI MsgSheetSolutionFrom "Datum, sonst nur für Korrektoren"
|
|
||||||
& setTooltip MsgSheetSolutionFromTip) (sfSolutionFrom <$> template)
|
|
||||||
<* aformSection MsgSheetFormFiles
|
<* aformSection MsgSheetFormFiles
|
||||||
<*> aopt (multiFileField $ oldFileIds SheetExercise) (fslI MsgSheetExercise) (sfSheetF <$> template)
|
<*> aopt (multiFileField $ oldFileIds SheetExercise) (fslI MsgSheetExercise) (sfSheetF <$> template)
|
||||||
<*> aopt (multiFileField $ oldFileIds SheetHint) (fslI MsgSheetHint) (sfHintF <$> template)
|
<*> aopt (multiFileField $ oldFileIds SheetHint) (fslI MsgSheetHint) (sfHintF <$> template)
|
||||||
<*> aopt (multiFileField $ oldFileIds SheetSolution) (fslI MsgSheetSolution) (sfSolutionF <$> template)
|
<*> aopt (multiFileField $ oldFileIds SheetSolution) (fslI MsgSheetSolution) (sfSolutionF <$> template)
|
||||||
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarkingFiles
|
<*> aopt (multiFileField $ oldFileIds SheetMarking) (fslI MsgSheetMarkingFiles
|
||||||
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
& setTooltip MsgSheetMarkingTip) (sfMarkingF <$> template)
|
||||||
|
<* aformSection MsgSheetFormTimes
|
||||||
|
<*> aopt utcTimeField (fslI MsgSheetVisibleFrom
|
||||||
|
& setTooltip MsgSheetVisibleFromTip)
|
||||||
|
((sfVisibleFrom <$> template) <|> pure (Just ctime))
|
||||||
|
<*> aopt utcTimeField (fslI MsgSheetActiveFrom
|
||||||
|
& setTooltip MsgSheetActiveFromTip)
|
||||||
|
(sfActiveFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslI MsgSheetActiveTo & setTooltip MsgSheetActiveToTip) (sfActiveTo <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgSheetHintFrom (mr MsgSheetHintFromPlaceholder)
|
||||||
|
& setTooltip MsgSheetHintFromTip) (sfHintFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgSheetSolutionFrom (mr MsgSheetSolutionFromPlaceholder)
|
||||||
|
& setTooltip MsgSheetSolutionFromTip) (sfSolutionFrom <$> template)
|
||||||
<* aformSection MsgSheetFormType
|
<* aformSection MsgSheetFormType
|
||||||
<*> sheetTypeAFormReq (fslI MsgSheetType
|
<*> sheetTypeAFormReq (fslI MsgSheetType
|
||||||
& setTooltip (uniworxMessages [MsgSheetTypeInfoBonus, MsgSheetTypeInfoInformational, MsgSheetTypeInfoNotGraded]))
|
& setTooltip (uniworxMessages [MsgSheetTypeInfoBonus, MsgSheetTypeInfoInformational, MsgSheetTypeInfoNotGraded]))
|
||||||
(sfType <$> template)
|
(sfType <$> template)
|
||||||
<*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template)
|
<*> sheetGroupAFormReq (fslI MsgSheetGroup) (sfGrouping <$> template)
|
||||||
<*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction))
|
<*> submissionModeForm ((sfSubmissionMode <$> template) <|> pure (SubmissionMode False . Just $ UploadAny True defaultExtensionRestriction))
|
||||||
<*> aopt htmlField (fslpI MsgSheetDescription "Html")
|
<*> apopt checkBoxField (fslI MsgAutoAssignCorrs) (sfAutoDistribute <$> template)
|
||||||
(sfDescription <$> template)
|
|
||||||
<*> aopt htmlField (fslpI MsgSheetMarking "Html") (sfMarkingText <$> template)
|
<*> aopt htmlField (fslpI MsgSheetMarking "Html") (sfMarkingText <$> template)
|
||||||
|
<*> correctorForm (fromMaybe mempty $ sfCorrectors <$> template)
|
||||||
return $ case result of
|
return $ case result of
|
||||||
FormSuccess sheetResult
|
FormSuccess sheetResult
|
||||||
| errorMsgs <- validateSheet mr sheetResult
|
| errorMsgs <- validateSheet mr' sheetResult
|
||||||
, not $ null errorMsgs ->
|
, not $ null errorMsgs ->
|
||||||
(FormFailure errorMsgs, widget)
|
(FormFailure errorMsgs, widget)
|
||||||
_ -> (result, widget)
|
_ -> (result, widget)
|
||||||
@ -132,10 +151,10 @@ makeSheetForm msId template = identifyForm FIDsheet $ \html -> do
|
|||||||
validateSheet :: MsgRenderer -> SheetForm -> [Text]
|
validateSheet :: MsgRenderer -> SheetForm -> [Text]
|
||||||
validateSheet (MsgRenderer {..}) (SheetForm{..}) =
|
validateSheet (MsgRenderer {..}) (SheetForm{..}) =
|
||||||
[ msg | (False, msg) <-
|
[ msg | (False, msg) <-
|
||||||
[ ( sfVisibleFrom <= Just sfActiveFrom , render MsgSheetErrVisibility)
|
[ ( NTop sfVisibleFrom <= NTop sfActiveFrom , render MsgSheetErrVisibility)
|
||||||
, ( sfActiveFrom <= sfActiveTo , render MsgSheetErrDeadlineEarly)
|
, ( NTop sfActiveFrom <= NTop sfActiveTo , render MsgSheetErrDeadlineEarly)
|
||||||
, ( NTop sfHintFrom >= NTop (Just sfActiveFrom) , render MsgSheetErrHintEarly)
|
, ( NTop sfHintFrom >= NTop sfActiveFrom , render MsgSheetErrHintEarly)
|
||||||
, ( NTop sfSolutionFrom >= NTop (Just sfActiveTo) , render MsgSheetErrSolutionEarly)
|
, ( NTop sfSolutionFrom >= NTop sfActiveTo , render MsgSheetErrSolutionEarly)
|
||||||
] ]
|
] ]
|
||||||
|
|
||||||
|
|
||||||
@ -216,9 +235,9 @@ getSheetListR tid ssh csh = do
|
|||||||
else spacerCell
|
else spacerCell
|
||||||
] id & cellAttrs <>~ [("class","list--inline list--space-separated")]
|
] id & cellAttrs <>~ [("class","list--inline list--space-separated")]
|
||||||
, sortable (Just "submission-since") (i18nCell MsgSheetActiveFrom)
|
, sortable (Just "submission-since") (i18nCell MsgSheetActiveFrom)
|
||||||
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> dateTimeCell sheetActiveFrom
|
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> maybe mempty dateTimeCell sheetActiveFrom
|
||||||
, sortable (Just "submission-until") (i18nCell MsgSheetActiveTo)
|
, sortable (Just "submission-until") (i18nCell MsgSheetActiveTo)
|
||||||
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> dateTimeCell sheetActiveTo
|
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> maybe mempty dateTimeCell sheetActiveTo
|
||||||
, sortable Nothing (i18nCell MsgSheetType)
|
, sortable Nothing (i18nCell MsgSheetType)
|
||||||
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> i18nCell sheetType
|
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, _, _)} -> i18nCell sheetType
|
||||||
, sortable Nothing (i18nCell MsgSubmission)
|
, sortable Nothing (i18nCell MsgSubmission)
|
||||||
@ -231,26 +250,34 @@ getSheetListR tid ssh csh = do
|
|||||||
return $ CSubmissionR tid ssh csh sheetName cid' SubShowR
|
return $ CSubmissionR tid ssh csh sheetName cid' SubShowR
|
||||||
in anchorCellM mkRoute (mkCid >>= \cid2 -> [whamlet|#{cid2}|])
|
in anchorCellM mkRoute (mkCid >>= \cid2 -> [whamlet|#{cid2}|])
|
||||||
, sortable (Just "rating") (i18nCell MsgRating)
|
, sortable (Just "rating") (i18nCell MsgRating)
|
||||||
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, mbSub,_)} ->
|
$ \DBRow{dbrOutput=(Entity _ Sheet{..}, _, mbSub,_)} ->
|
||||||
let stats = sheetTypeSum sheetType in -- for statistics over all shown rows
|
let stats = sheetTypeSum sheetType in -- for statistics over all shown rows
|
||||||
case mbSub of
|
case mbSub of
|
||||||
Nothing -> cellTell mempty $ stats Nothing
|
Nothing -> cellTell mempty $ stats Nothing
|
||||||
(Just (Entity sid sub@Submission{..})) ->
|
(Just (Entity sid sub@Submission{..})) ->
|
||||||
let mkCid = encrypt sid
|
let
|
||||||
mkRoute = do
|
mkRoute :: (MonadHandler m, HandlerSite m ~ UniWorX) => m (Route UniWorX)
|
||||||
cid' <- mkCid
|
mkRoute = liftHandler $ do
|
||||||
|
cid' <- encrypt sid
|
||||||
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
return $ CSubmissionR tid ssh csh sheetName cid' CorrectionR
|
||||||
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
mTuple mA mB = (,) <$> mA <*> mB -- Hamlet does not support enough haskell-syntax for this
|
||||||
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
acell = anchorCellM mkRoute $(widgetFile "widgets/rating/rating")
|
||||||
in cellTell acell $ stats submissionRatingPoints
|
tellStats = do
|
||||||
|
r <- mkRoute
|
||||||
|
showRating <- hasReadAccessTo r
|
||||||
|
tell . stats $ bool Nothing submissionRatingPoints showRating
|
||||||
|
in acell & cellContents %~ (<* tellStats)
|
||||||
|
|
||||||
, sortable Nothing -- (Just "percent")
|
, sortable Nothing -- (Just "percent")
|
||||||
(i18nCell MsgRatingPercent)
|
(i18nCell MsgRatingPercent)
|
||||||
$ \DBRow{dbrOutput=(Entity _ Sheet{sheetType=sType}, _, mbSub,_)} -> case mbSub of
|
$ \DBRow{dbrOutput=(Entity _ Sheet{sheetType=sType, sheetName}, _, mbSub,_)} -> case mbSub of
|
||||||
(Just (Entity _ Submission{submissionRatingPoints=Just sPoints})) ->
|
(Just (Entity sid Submission{submissionRatingPoints=Just sPoints})) ->
|
||||||
case preview (_grading . _maxPoints) sType of
|
case preview (_grading . _maxPoints) sType of
|
||||||
Just maxPoints
|
Just maxPoints
|
||||||
| maxPoints /= 0 -> textCell $ textPercent sPoints maxPoints
|
| maxPoints /= 0 -> cell $ do
|
||||||
|
cID <- encrypt sid
|
||||||
|
showRating <- hasReadAccessTo $ CSubmissionR tid ssh csh sheetName cID CorrectionR
|
||||||
|
bool (return ()) (toWidget . toMessage $ textPercent sPoints maxPoints) showRating
|
||||||
_other -> mempty
|
_other -> mempty
|
||||||
_other -> mempty
|
_other -> mempty
|
||||||
]
|
]
|
||||||
@ -319,17 +346,6 @@ getSheetListR tid ssh csh = do
|
|||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
$(widgetFile "sheetList")
|
$(widgetFile "sheetList")
|
||||||
|
|
||||||
data ButtonGeneratePseudonym = BtnGenerate
|
|
||||||
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
|
||||||
instance Universe ButtonGeneratePseudonym
|
|
||||||
instance Finite ButtonGeneratePseudonym
|
|
||||||
|
|
||||||
nullaryPathPiece ''ButtonGeneratePseudonym (camelToPathPiece' 1)
|
|
||||||
|
|
||||||
instance Button UniWorX ButtonGeneratePseudonym where
|
|
||||||
btnLabel BtnGenerate = [whamlet|_{MsgSheetGeneratePseudonym}|]
|
|
||||||
btnClasses BtnGenerate = [BCIsButton, BCDefault]
|
|
||||||
|
|
||||||
-- Show single sheet
|
-- Show single sheet
|
||||||
getSShowR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSShowR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
getSShowR tid ssh csh shn = do
|
getSShowR tid ssh csh shn = do
|
||||||
@ -422,8 +438,9 @@ getSShowR tid ssh csh shn = do
|
|||||||
setTitleI $ prependCourseTitle tid ssh csh $ SomeMessage shn
|
setTitleI $ prependCourseTitle tid ssh csh $ SomeMessage shn
|
||||||
let zipLink = CSheetR tid ssh csh shn SArchiveR
|
let zipLink = CSheetR tid ssh csh shn SArchiveR
|
||||||
visibleFrom = visibleUTCTime SelFormatDateTime <$> sheetVisibleFrom sheet
|
visibleFrom = visibleUTCTime SelFormatDateTime <$> sheetVisibleFrom sheet
|
||||||
sheetFrom <- formatTime SelFormatDateTime $ sheetActiveFrom sheet
|
hasSubmission = classifySubmissionMode (sheetSubmissionMode sheet) /= SubmissionModeNone
|
||||||
sheetTo <- formatTime SelFormatDateTime $ sheetActiveTo sheet
|
sheetFrom <- traverse (formatTime SelFormatDateTime) $ sheetActiveFrom sheet
|
||||||
|
sheetTo <- traverse (formatTime SelFormatDateTime) $ sheetActiveTo sheet
|
||||||
hintsFrom <- traverse (formatTime SelFormatDateTime) $ sheetHintFrom sheet
|
hintsFrom <- traverse (formatTime SelFormatDateTime) $ sheetHintFrom sheet
|
||||||
solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet
|
solutionFrom <- traverse (formatTime SelFormatDateTime) $ sheetSolutionFrom sheet
|
||||||
markingText <- runMaybeT $ assertM_ (Authorized ==) (evalAccessCorrector tid ssh csh) >> hoistMaybe (sheetMarkingText sheet)
|
markingText <- runMaybeT $ assertM_ (Authorized ==) (evalAccessCorrector tid ssh csh) >> hoistMaybe (sheetMarkingText sheet)
|
||||||
@ -480,7 +497,8 @@ getSheetNewR tid ssh csh = do
|
|||||||
(FormSuccess (Just shn)) -> E.where_ $ sheet E.^. SheetName E.==. E.val shn
|
(FormSuccess (Just shn)) -> E.where_ $ sheet E.^. SheetName E.==. E.val shn
|
||||||
-- (FormFailure msgs) -> -- not in MonadHandler anymore -- forM_ msgs (addMessage Error . toHtml)
|
-- (FormFailure msgs) -> -- not in MonadHandler anymore -- forM_ msgs (addMessage Error . toHtml)
|
||||||
_other -> return ()
|
_other -> return ()
|
||||||
lastSheets <- runDB $ E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
|
(lastSheets, loads) <- runDB $ do
|
||||||
|
lSheets <- E.select . E.from $ \(course `E.InnerJoin` sheet) -> do
|
||||||
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
E.on $ course E.^. CourseId E.==. sheet E.^. SheetCourse
|
||||||
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
||||||
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
@ -493,27 +511,35 @@ getSheetNewR tid ssh csh = do
|
|||||||
-- E.orderBy [E.desc lastSheetEdit, E.desc (sheet E.^. SheetActiveFrom)]
|
-- E.orderBy [E.desc lastSheetEdit, E.desc (sheet E.^. SheetActiveFrom)]
|
||||||
E.orderBy [E.desc (sheet E.^. SheetActiveFrom)]
|
E.orderBy [E.desc (sheet E.^. SheetActiveFrom)]
|
||||||
E.limit 1
|
E.limit 1
|
||||||
return sheet
|
let firstEdit = E.sub_select . E.from $ \sheetEdit -> do
|
||||||
|
E.where_ $ sheetEdit E.^. SheetEditSheet E.==. sheet E.^. SheetId
|
||||||
|
return . E.min_ $ sheetEdit E.^. SheetEditTime
|
||||||
|
return (sheet, firstEdit)
|
||||||
|
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
loads <- defaultLoads cid
|
||||||
|
return (lSheets, loads)
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let template = case lastSheets of
|
let template = case lastSheets of
|
||||||
((Entity {entityVal=Sheet{..}}):_) ->
|
((Entity {entityVal=Sheet{..}}, E.Value fEdit):_) ->
|
||||||
let addTime = addWeeks $ max 1 $ weeksToAdd sheetActiveTo now
|
let addTime = addWeeks $ max 1 $ weeksToAdd (fromMaybe (UTCTime systemEpochDay 0) $ sheetActiveTo <|> fEdit) now
|
||||||
in Just $ SheetForm
|
in Just $ SheetForm
|
||||||
{ sfName = stepTextCounterCI sheetName
|
{ sfName = stepTextCounterCI sheetName
|
||||||
, sfDescription = sheetDescription
|
, sfDescription = sheetDescription
|
||||||
, sfType = sheetType
|
, sfType = sheetType
|
||||||
, sfGrouping = sheetGrouping
|
, sfGrouping = sheetGrouping
|
||||||
, sfVisibleFrom = addTime <$> sheetVisibleFrom
|
, sfVisibleFrom = addTime <$> sheetVisibleFrom
|
||||||
, sfActiveFrom = addTime sheetActiveFrom
|
, sfActiveFrom = addTime <$> sheetActiveFrom
|
||||||
, sfActiveTo = addTime sheetActiveTo
|
, sfActiveTo = addTime <$> sheetActiveTo
|
||||||
, sfSubmissionMode = sheetSubmissionMode
|
, sfSubmissionMode = sheetSubmissionMode
|
||||||
, sfSheetF = Nothing
|
, sfSheetF = Nothing
|
||||||
, sfHintFrom = addTime <$> sheetHintFrom
|
, sfHintFrom = addTime <$> sheetHintFrom
|
||||||
, sfHintF = Nothing
|
, sfHintF = Nothing
|
||||||
, sfSolutionFrom = addTime <$> sheetSolutionFrom
|
, sfSolutionFrom = addTime <$> sheetSolutionFrom
|
||||||
, sfSolutionF = Nothing
|
, sfSolutionF = Nothing
|
||||||
, sfMarkingF = Nothing
|
, sfMarkingF = Nothing
|
||||||
, sfMarkingText = sheetMarkingText
|
, sfMarkingText = sheetMarkingText
|
||||||
|
, sfAutoDistribute = sheetAutoDistribute
|
||||||
|
, sfCorrectors = loads
|
||||||
}
|
}
|
||||||
_other -> Nothing
|
_other -> Nothing
|
||||||
let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
|
let action newSheet = -- More specific error message for new sheet could go here, if insertUnique returns Nothing
|
||||||
@ -526,44 +552,49 @@ postSheetNewR = getSheetNewR
|
|||||||
|
|
||||||
getSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
getSEditR tid ssh csh shn = do
|
getSEditR tid ssh csh shn = do
|
||||||
(Entity sid Sheet{..}, sheetFileIds) <- runDB $ do
|
(Entity sid Sheet{..}, sheetFileIds, currentLoads) <- runDB $ do
|
||||||
ent <- fetchSheet tid ssh csh shn
|
ent@(Entity sid _) <- fetchSheet tid ssh csh shn
|
||||||
fti <- getFtIdMap $ entityKey ent
|
fti <- getFtIdMap $ entityKey ent
|
||||||
return (ent, fti)
|
cLoads <- Map.union
|
||||||
|
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (InvDBDataSheetCorrector sheetCorrectorLoad sheetCorrectorState, InvTokenDataSheetCorrector)) (selectList [ SheetCorrectorSheet ==. sid ] [])
|
||||||
|
<*> fmap (fmap (, InvTokenDataSheetCorrector) . Map.mapKeysMonotonic Left) (sourceInvitationsF sid)
|
||||||
|
return (ent, fti, cLoads)
|
||||||
let template = Just $ SheetForm
|
let template = Just $ SheetForm
|
||||||
{ sfName = sheetName
|
{ sfName = sheetName
|
||||||
, sfDescription = sheetDescription
|
, sfDescription = sheetDescription
|
||||||
, sfType = sheetType
|
, sfType = sheetType
|
||||||
, sfGrouping = sheetGrouping
|
, sfGrouping = sheetGrouping
|
||||||
, sfVisibleFrom = sheetVisibleFrom
|
, sfVisibleFrom = sheetVisibleFrom
|
||||||
, sfActiveFrom = sheetActiveFrom
|
, sfActiveFrom = sheetActiveFrom
|
||||||
, sfActiveTo = sheetActiveTo
|
, sfActiveTo = sheetActiveTo
|
||||||
, sfSubmissionMode = sheetSubmissionMode
|
, sfSubmissionMode = sheetSubmissionMode
|
||||||
, sfSheetF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetExercise
|
, sfSheetF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetExercise
|
||||||
, sfHintFrom = sheetHintFrom
|
, sfHintFrom = sheetHintFrom
|
||||||
, sfHintF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetHint
|
, sfHintF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetHint
|
||||||
, sfSolutionFrom = sheetSolutionFrom
|
, sfSolutionFrom = sheetSolutionFrom
|
||||||
, sfSolutionF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetSolution
|
, sfSolutionF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetSolution
|
||||||
, sfMarkingF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetMarking
|
, sfMarkingF = Just . yieldMany . map Left . Set.elems $ sheetFileIds SheetMarking
|
||||||
, sfMarkingText = sheetMarkingText
|
, sfMarkingText = sheetMarkingText
|
||||||
|
, sfAutoDistribute = sheetAutoDistribute
|
||||||
|
, sfCorrectors = currentLoads
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = uniqueReplace sid -- More specific error message for edit old sheet could go here by using myReplaceUnique instead
|
let action = uniqueReplace sid -- More specific error message for edit old sheet could go here by using myReplaceUnique instead
|
||||||
handleSheetEdit tid ssh csh (Just sid) template action
|
handleSheetEdit tid ssh csh (Just sid) template action
|
||||||
|
|
||||||
postSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
postSEditR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
postSEditR = getSEditR
|
postSEditR = getSEditR
|
||||||
|
|
||||||
handleSheetEdit :: TermId -> SchoolId -> CourseShorthand -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodDB UniWorX (Maybe SheetId)) -> Handler Html
|
handleSheetEdit :: TermId -> SchoolId -> CourseShorthand -> Maybe SheetId -> Maybe SheetForm -> (Sheet -> YesodJobDB UniWorX (Maybe SheetId)) -> Handler Html
|
||||||
handleSheetEdit tid ssh csh msId template dbAction = do
|
handleSheetEdit tid ssh csh msId template dbAction = do
|
||||||
let mbshn = sfName <$> template
|
let mbshn = sfName <$> template
|
||||||
aid <- requireAuthId
|
aid <- requireAuthId
|
||||||
|
cid <- runDB $ getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template
|
((res,formWidget), formEnctype) <- runFormPost $ makeSheetForm msId template
|
||||||
case res of
|
case res of
|
||||||
(FormSuccess SheetForm{..}) -> do
|
(FormSuccess SheetForm{..}) -> do
|
||||||
saveOkay <- runDB $ do
|
saveOkay <- runDBJobs $ do
|
||||||
actTime <- liftIO getCurrentTime
|
actTime <- liftIO getCurrentTime
|
||||||
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
|
|
||||||
oldAutoDistribute <- fmap sheetAutoDistribute . join <$> traverse get msId
|
|
||||||
let newSheet = Sheet
|
let newSheet = Sheet
|
||||||
{ sheetCourse = cid
|
{ sheetCourse = cid
|
||||||
, sheetName = sfName
|
, sheetName = sfName
|
||||||
@ -577,7 +608,7 @@ handleSheetEdit tid ssh csh msId template dbAction = do
|
|||||||
, sheetHintFrom = sfHintFrom
|
, sheetHintFrom = sfHintFrom
|
||||||
, sheetSolutionFrom = sfSolutionFrom
|
, sheetSolutionFrom = sfSolutionFrom
|
||||||
, sheetSubmissionMode = sfSubmissionMode
|
, sheetSubmissionMode = sfSubmissionMode
|
||||||
, sheetAutoDistribute = fromMaybe False oldAutoDistribute
|
, sheetAutoDistribute = sfAutoDistribute
|
||||||
}
|
}
|
||||||
mbsid <- dbAction newSheet
|
mbsid <- dbAction newSheet
|
||||||
case mbsid of
|
case mbsid of
|
||||||
@ -590,22 +621,36 @@ handleSheetEdit tid ssh csh msId template dbAction = do
|
|||||||
insert_ $ SheetEdit aid actTime sid
|
insert_ $ SheetEdit aid actTime sid
|
||||||
addMessageI Success $ MsgSheetEditOk tid ssh csh sfName
|
addMessageI Success $ MsgSheetEditOk tid ssh csh sfName
|
||||||
-- Sanity checks generating warnings only, but not errors!
|
-- Sanity checks generating warnings only, but not errors!
|
||||||
warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <-
|
hoist lift . warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <-
|
||||||
[ (sfVisibleFrom, MsgSheetVisibleFrom)
|
[ (sfVisibleFrom, MsgSheetVisibleFrom)
|
||||||
, (Just sfActiveFrom, MsgSheetActiveFrom)
|
, (sfActiveFrom, MsgSheetActiveFrom)
|
||||||
, (Just sfActiveTo, MsgSheetActiveTo)
|
, (sfActiveTo, MsgSheetActiveTo)
|
||||||
, (sfHintFrom, MsgSheetSolutionFromTip)
|
, (sfHintFrom, MsgSheetSolutionFromTip)
|
||||||
, (sfSolutionFrom, MsgSheetSolutionFrom)
|
, (sfSolutionFrom, MsgSheetSolutionFrom)
|
||||||
] ]
|
] ]
|
||||||
|
|
||||||
|
let
|
||||||
|
sheetCorrectors :: Set (Either (Invitation' SheetCorrector) SheetCorrector)
|
||||||
|
sheetCorrectors = Set.fromList . map f $ Map.toList sfCorrectors
|
||||||
|
where
|
||||||
|
f (Left email, invData) = Left (email, sid, invData)
|
||||||
|
f (Right uid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector)) = Right $ SheetCorrector uid sid load cState
|
||||||
|
(invites, adds) = partitionEithers $ Set.toList sheetCorrectors
|
||||||
|
|
||||||
|
deleteWhere [ SheetCorrectorSheet ==. sid ]
|
||||||
|
insertMany_ adds
|
||||||
|
|
||||||
|
deleteWhere [InvitationFor ==. invRef @SheetCorrector sid, InvitationEmail /<-. toListOf (folded . _1) invites]
|
||||||
|
sinkInvitationsF correctorInvitationConfig invites
|
||||||
|
|
||||||
return True
|
return True
|
||||||
when saveOkay $ redirect $ case msId of
|
when saveOkay $
|
||||||
Just _ -> CSheetR tid ssh csh sfName SShowR -- redirect must happen outside of runDB
|
redirect $ CSheetR tid ssh csh sfName SShowR -- redirect must happen outside of runDB
|
||||||
Nothing -> CSheetR tid ssh csh sfName SCorrR
|
|
||||||
(FormFailure msgs) -> forM_ msgs $ (addMessage Error) . toHtml
|
(FormFailure msgs) -> forM_ msgs $ (addMessage Error) . toHtml
|
||||||
_ -> runDB $ warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <-
|
_ -> runDB $ warnTermDays tid $ Map.fromList [ (date,name) | (Just date, name) <-
|
||||||
[(sfVisibleFrom =<< template, MsgSheetVisibleFrom)
|
[(sfVisibleFrom =<< template, MsgSheetVisibleFrom)
|
||||||
,(sfActiveFrom <$> template, MsgSheetActiveFrom)
|
,(sfActiveFrom =<< template, MsgSheetActiveFrom)
|
||||||
,(sfActiveTo <$> template, MsgSheetActiveTo)
|
,(sfActiveTo =<< template, MsgSheetActiveTo)
|
||||||
,(sfHintFrom =<< template, MsgSheetSolutionFromTip)
|
,(sfHintFrom =<< template, MsgSheetSolutionFromTip)
|
||||||
,(sfSolutionFrom =<< template, MsgSheetSolutionFrom)
|
,(sfSolutionFrom =<< template, MsgSheetSolutionFrom)
|
||||||
] ]
|
] ]
|
||||||
@ -641,14 +686,14 @@ insertSheetFile sid ftype finfo = do
|
|||||||
fid <- insert file
|
fid <- insert file
|
||||||
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
|
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
|
||||||
|
|
||||||
insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodDB UniWorX ()
|
insertSheetFile' :: SheetId -> SheetFileType -> ConduitT () (Either FileId File) Handler () -> YesodJobDB UniWorX ()
|
||||||
insertSheetFile' sid ftype fs = do
|
insertSheetFile' sid ftype fs = do
|
||||||
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
|
oldFileIds <- fmap setFromList . fmap (map E.unValue) . E.select . E.from $ \(file `E.InnerJoin` sheetFile) -> do
|
||||||
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
|
E.on $ file E.^. FileId E.==. sheetFile E.^. SheetFileFile
|
||||||
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
|
E.where_ $ sheetFile E.^. SheetFileSheet E.==. E.val sid
|
||||||
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
|
E.&&. sheetFile E.^. SheetFileType E.==. E.val ftype
|
||||||
return (file E.^. FileId)
|
return (file E.^. FileId)
|
||||||
keep <- execWriterT . runConduit $ transPipe (lift . lift) fs .| C.mapM_ finsert
|
keep <- execWriterT . runConduit $ transPipe liftHandler fs .| C.mapM_ finsert
|
||||||
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
|
mapM_ deleteCascade $ (oldFileIds \\ keep :: Set FileId)
|
||||||
where
|
where
|
||||||
finsert (Left fileId) = tell $ singleton fileId
|
finsert (Left fileId) = tell $ singleton fileId
|
||||||
@ -657,22 +702,12 @@ insertSheetFile' sid ftype fs = do
|
|||||||
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
|
void . insert $ SheetFile sid fid ftype -- cannot fail due to uniqueness, since we generated a fresh FileId in the previous step
|
||||||
|
|
||||||
|
|
||||||
data CorrectorForm = CorrectorForm
|
defaultLoads :: CourseId -> DB Loads
|
||||||
{ cfUserId :: UserId
|
|
||||||
, cfUserName :: Text
|
|
||||||
, cfResult :: FormResult (CorrectorState, Load)
|
|
||||||
, cfViewByTut, cfViewProp, cfViewDel, cfViewState :: FieldView UniWorX
|
|
||||||
}
|
|
||||||
|
|
||||||
type Loads = Map (Either UserEmail UserId) (CorrectorState, Load)
|
|
||||||
|
|
||||||
defaultLoads :: SheetId -> DB Loads
|
|
||||||
-- ^ Generate `Loads` in such a way that minimal editing is required
|
-- ^ Generate `Loads` in such a way that minimal editing is required
|
||||||
--
|
--
|
||||||
-- For every user, that ever was a corrector for this course, return their last `Load`.
|
-- For every user, that ever was a corrector for this course, return their last `Load`.
|
||||||
-- "Last `Load`" is taken to mean their `Load` on the `Sheet` with the most recent creation time (first edit).
|
-- "Last `Load`" is taken to mean their `Load` on the `Sheet` with the most recent creation time (first edit).
|
||||||
defaultLoads shid = do
|
defaultLoads cId = do
|
||||||
cId <- sheetCourse <$> getJust shid
|
|
||||||
fmap toMap . E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> E.distinctOnOrderBy [E.asc (sheetCorrector E.^. SheetCorrectorUser)] $ do
|
fmap toMap . E.select . E.from $ \(sheet `E.InnerJoin` sheetCorrector) -> E.distinctOnOrderBy [E.asc (sheetCorrector E.^. SheetCorrectorUser)] $ do
|
||||||
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
|
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
|
||||||
|
|
||||||
@ -687,37 +722,20 @@ defaultLoads shid = do
|
|||||||
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
|
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
|
||||||
where
|
where
|
||||||
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
|
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
|
||||||
toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (cState, cLoad)
|
toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (InvDBDataSheetCorrector cLoad cState, InvTokenDataSheetCorrector)
|
||||||
|
|
||||||
|
|
||||||
correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector))
|
correctorForm :: Loads -> AForm Handler Loads
|
||||||
correctorForm shid = wFormToAForm $ do
|
correctorForm loads' = wFormToAForm $ do
|
||||||
currentRoute <- fromMaybe (error "correctorForm called from 404-handler") <$> liftHandler getCurrentRoute
|
currentRoute <- fromMaybe (error "correctorForm called from 404-handler") <$> liftHandler getCurrentRoute
|
||||||
userId <- liftHandler requireAuthId
|
userId <- liftHandler requireAuthId
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
let
|
let
|
||||||
currentLoads :: DB Loads
|
|
||||||
currentLoads = Map.union
|
|
||||||
<$> fmap (foldMap $ \(Entity _ SheetCorrector{..}) -> Map.singleton (Right sheetCorrectorUser) (sheetCorrectorState, sheetCorrectorLoad)) (selectList [ SheetCorrectorSheet ==. shid ] [])
|
|
||||||
<*> fmap (fmap ((,) <$> invDBSheetCorrectorState <*> invDBSheetCorrectorLoad) . Map.mapKeysMonotonic Left) (sourceInvitationsF shid)
|
|
||||||
(defaultLoads', currentLoads') <- liftHandler . runDB $ (,) <$> defaultLoads shid <*> currentLoads
|
|
||||||
|
|
||||||
isWrite <- liftHandler $ isWriteRequest currentRoute
|
|
||||||
|
|
||||||
let
|
|
||||||
applyDefaultLoads = Map.null currentLoads' && not isWrite
|
|
||||||
loads :: Map (Either UserEmail UserId) (CorrectorState, Load)
|
loads :: Map (Either UserEmail UserId) (CorrectorState, Load)
|
||||||
loads
|
loads = loads' <&> \(InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector) -> (cState, load)
|
||||||
| applyDefaultLoads = defaultLoads'
|
|
||||||
| otherwise = currentLoads'
|
|
||||||
|
|
||||||
countTutRes <- wreq checkBoxField (fslI MsgCountTutProp & setTooltip MsgCountTutPropTip) . Just . any (\(_, Load{..}) -> fromMaybe False byTutorial) $ Map.elems loads
|
countTutRes <- wpopt checkBoxField (fslI MsgCountTutProp & setTooltip MsgCountTutPropTip) . Just . any (\(_, Load{..}) -> fromMaybe False byTutorial) $ Map.elems loads
|
||||||
|
|
||||||
-- when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Message
|
|
||||||
-- addMessageI Warning MsgCorrectorsDefaulted
|
|
||||||
when (not (Map.null loads) && applyDefaultLoads) $ -- Alert Notification
|
|
||||||
wformMessage =<< messageIconI Warning IconNoCorrectors MsgCorrectorsDefaulted
|
|
||||||
|
|
||||||
|
|
||||||
let
|
let
|
||||||
@ -804,51 +822,16 @@ correctorForm shid = wFormToAForm $ do
|
|||||||
miIdent :: Text
|
miIdent :: Text
|
||||||
miIdent = "correctors"
|
miIdent = "correctors"
|
||||||
|
|
||||||
postProcess :: Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)) -> Set (Either (Invitation' SheetCorrector) SheetCorrector)
|
postProcess :: Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)) -> Loads
|
||||||
postProcess = Set.fromList . map postProcess' . Map.elems
|
postProcess = Map.fromList . map postProcess' . Map.elems
|
||||||
where
|
where
|
||||||
sheetCorrectorSheet = shid
|
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> (Either UserEmail UserId, (InvitationDBData SheetCorrector, InvitationTokenData SheetCorrector))
|
||||||
|
postProcess' = over _2 $ \(cState, load) -> (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector)
|
||||||
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector
|
|
||||||
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
|
|
||||||
postProcess' (Left email, (cState, load)) = Left (email, shid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector))
|
|
||||||
|
|
||||||
filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
|
filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
|
||||||
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
|
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
|
||||||
|
|
||||||
fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) True filledData
|
fmap postProcess <$> massInputW MassInput{..} (fslI MsgCorrectors & setTooltip MsgMassInputTip) False filledData
|
||||||
|
|
||||||
getSCorrR, postSCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
|
||||||
postSCorrR = getSCorrR
|
|
||||||
getSCorrR tid ssh csh shn = do
|
|
||||||
Entity shid Sheet{..} <- runDB $ fetchSheet tid ssh csh shn
|
|
||||||
|
|
||||||
((res,formWidget), formEnctype) <- runFormPost . identifyForm FIDcorrectors . renderAForm FormStandard $
|
|
||||||
(,) <$> areq checkBoxField (fslI MsgAutoAssignCorrs) (Just sheetAutoDistribute)
|
|
||||||
<*> correctorForm shid
|
|
||||||
|
|
||||||
case res of
|
|
||||||
FormFailure errs -> mapM_ (addMessage Error . toHtml) errs
|
|
||||||
FormSuccess (autoDistribute, sheetCorrectors) -> runDBJobs $ do
|
|
||||||
update shid [ SheetAutoDistribute =. autoDistribute ]
|
|
||||||
|
|
||||||
let (invites, adds) = partitionEithers $ Set.toList sheetCorrectors
|
|
||||||
|
|
||||||
deleteWhere [ SheetCorrectorSheet ==. shid ]
|
|
||||||
insertMany_ adds
|
|
||||||
|
|
||||||
deleteWhere [InvitationFor ==. invRef @SheetCorrector shid, InvitationEmail /<-. toListOf (folded . _1) invites]
|
|
||||||
sinkInvitationsF correctorInvitationConfig invites
|
|
||||||
|
|
||||||
addMessageI Success MsgCorrectorsUpdated
|
|
||||||
FormMissing -> return ()
|
|
||||||
|
|
||||||
defaultLayout $ do
|
|
||||||
setTitleI $ MsgSheetCorrectorsTitle tid ssh csh shn
|
|
||||||
wrapForm formWidget def
|
|
||||||
{ formAction = Just . SomeRoute $ CSheetR tid ssh csh shn SCorrR
|
|
||||||
, formEncoding = formEnctype
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
instance IsInvitableJunction SheetCorrector where
|
instance IsInvitableJunction SheetCorrector where
|
||||||
@ -905,7 +888,7 @@ correctorInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName
|
invitationHeading (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgSheetCorrInviteHeading sheetName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgSheetCorrInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
|
invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
|
||||||
@ -923,5 +906,5 @@ postSCorrInviteR = invitationR correctorInvitationConfig
|
|||||||
getSIsCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
getSIsCorrR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler Html
|
||||||
-- NOTE: The route SIsCorrR is only used to verfify corrector access rights to given sheet!
|
-- NOTE: The route SIsCorrR is only used to verfify corrector access rights to given sheet!
|
||||||
getSIsCorrR _ _ _ shn = do
|
getSIsCorrR _ _ _ shn = do
|
||||||
defaultLayout $ [whamlet|You have corrector access to #{shn}.|]
|
defaultLayout . i18n $ MsgHaveCorrectorAccess shn
|
||||||
|
|
||||||
|
|||||||
@ -107,7 +107,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
|
|||||||
invitationTokenConfig (Entity _ Submission{..}) _ = do
|
invitationTokenConfig (Entity _ Submission{..}) _ = do
|
||||||
Sheet{..} <- getJust submissionSheet
|
Sheet{..} <- getJust submissionSheet
|
||||||
Course{..} <- getJust sheetCourse
|
Course{..} <- getJust sheetCourse
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR)
|
itAddAuth <- either throwM (return . Just) $ routeAuthTags (CSheetR courseTerm courseSchool courseShorthand sheetName SubmissionNewR)
|
||||||
let itExpiresAt = Nothing
|
let itExpiresAt = Nothing
|
||||||
itStartsAt = Nothing
|
itStartsAt = Nothing
|
||||||
@ -125,7 +125,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
|
|||||||
return . SomeRoute $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SubShowR
|
return . SomeRoute $ CSubmissionR courseTerm courseSchool courseShorthand sheetName cID SubShowR
|
||||||
|
|
||||||
|
|
||||||
makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe (ConduitT () File Handler ()), Set (Either UserEmail UserId))
|
makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe FileUploads, Set (Either UserEmail UserId))
|
||||||
makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
|
makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
|
||||||
<$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
|
<$> fileUploadForm (not isLecturer && is _Nothing msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
|
||||||
<*> wFormToAForm submittorsForm
|
<*> wFormToAForm submittorsForm
|
||||||
@ -265,6 +265,8 @@ makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = ident
|
|||||||
|
|
||||||
postProcess :: Map ListPosition (Either UserEmail UserId, ()) -> Set (Either UserEmail UserId)
|
postProcess :: Map ListPosition (Either UserEmail UserId, ()) -> Set (Either UserEmail UserId)
|
||||||
postProcess = setOf $ folded . _1
|
postProcess = setOf $ folded . _1
|
||||||
|
when (maxSize > Just 1) $
|
||||||
|
wformMessage =<< messageI Info MsgCosubmittorTip
|
||||||
fmap postProcess <$> massInputW MassInput{..} submittorSettings' True (Just . Map.fromList . zip [0..] . map (, ()) $ Set.toList prefillUsers)
|
fmap postProcess <$> massInputW MassInput{..} submittorSettings' True (Just . Map.fromList . zip [0..] . map (, ()) $ Set.toList prefillUsers)
|
||||||
|
|
||||||
|
|
||||||
@ -428,7 +430,7 @@ submissionHelper tid ssh csh shn mcid = do
|
|||||||
(Nothing, Just smid) -- no new files, existing submission partners updated
|
(Nothing, Just smid) -- no new files, existing submission partners updated
|
||||||
-> return smid
|
-> return smid
|
||||||
(Just files, _) -> -- new files
|
(Just files, _) -> -- new files
|
||||||
runConduit $ transPipe (lift . lift) files .| extractRatingsMsg .| sinkSubmission uid (maybe (Left shid) Right msmid) False
|
runConduit $ transPipe (lift . lift) files .| Conduit.mapM (either get404 return) .| extractRatingsMsg .| sinkSubmission uid (maybe (Left shid) Right msmid) False
|
||||||
(Nothing, Nothing) -- new submission, no file upload requested
|
(Nothing, Nothing) -- new submission, no file upload requested
|
||||||
-> do
|
-> do
|
||||||
sid <- insert Submission
|
sid <- insert Submission
|
||||||
@ -484,7 +486,7 @@ submissionHelper tid ssh csh shn mcid = do
|
|||||||
| otherwise -> case change of
|
| otherwise -> case change of
|
||||||
Left subEmail -> deleteInvitation @SubmissionUser smid subEmail
|
Left subEmail -> deleteInvitation @SubmissionUser smid subEmail
|
||||||
Right subUid -> do
|
Right subUid -> do
|
||||||
deleteWhere [SubmissionUserUser ==. subUid]
|
deleteBy $ UniqueSubmissionUser subUid smid
|
||||||
audit $ TransactionSubmissionUserDelete smid subUid
|
audit $ TransactionSubmissionUserDelete smid subUid
|
||||||
|
|
||||||
addMessageI Success $ if | Nothing <- msmid -> MsgSubmissionCreated
|
addMessageI Success $ if | Nothing <- msmid -> MsgSubmissionCreated
|
||||||
|
|||||||
@ -23,6 +23,7 @@ postMessageR cID = do
|
|||||||
Nothing -> (systemMessageSummary, systemMessageContent)
|
Nothing -> (systemMessageSummary, systemMessageContent)
|
||||||
Just SystemMessageTranslation{..} -> (systemMessageTranslationSummary, systemMessageTranslationContent)
|
Just SystemMessageTranslation{..} -> (systemMessageTranslationSummary, systemMessageTranslationContent)
|
||||||
|
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
let
|
let
|
||||||
mkForm = do
|
mkForm = do
|
||||||
((modifyRes, modifyView), modifyEnctype) <- runFormPost . identifyForm FIDSystemMessageModify . renderAForm FormStandard
|
((modifyRes, modifyView), modifyEnctype) <- runFormPost . identifyForm FIDSystemMessageModify . renderAForm FormStandard
|
||||||
@ -31,9 +32,9 @@ postMessageR cID = do
|
|||||||
<*> aopt utcTimeField (fslI MsgSystemMessageTo) (Just systemMessageTo)
|
<*> aopt utcTimeField (fslI MsgSystemMessageTo) (Just systemMessageTo)
|
||||||
<*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) (Just systemMessageAuthenticatedOnly)
|
<*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) (Just systemMessageAuthenticatedOnly)
|
||||||
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) (Just systemMessageSeverity)
|
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) (Just systemMessageSeverity)
|
||||||
<*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageDefaultLanguage)
|
<*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just systemMessageDefaultLanguage)
|
||||||
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageContent)
|
<*> areq htmlField' (fslpI MsgSystemMessageContent "Html") (Just systemMessageContent)
|
||||||
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageSummary)
|
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") (Just systemMessageSummary)
|
||||||
|
|
||||||
ts <- runDB $ selectList [ SystemMessageTranslationMessage ==. smId ] [Asc SystemMessageTranslationLanguage]
|
ts <- runDB $ selectList [ SystemMessageTranslationMessage ==. smId ] [Asc SystemMessageTranslationLanguage]
|
||||||
let ts' = Map.fromList $ (systemMessageTranslationLanguage . entityVal &&& id) <$> ts
|
let ts' = Map.fromList $ (systemMessageTranslationLanguage . entityVal &&& id) <$> ts
|
||||||
@ -45,9 +46,9 @@ postMessageR cID = do
|
|||||||
<$> fmap (Entity tId)
|
<$> fmap (Entity tId)
|
||||||
( SystemMessageTranslation
|
( SystemMessageTranslation
|
||||||
<$> pure systemMessageTranslationMessage
|
<$> pure systemMessageTranslationMessage
|
||||||
<*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just systemMessageTranslationLanguage)
|
<*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just systemMessageTranslationLanguage)
|
||||||
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") (Just systemMessageTranslationContent)
|
<*> areq htmlField' (fslpI MsgSystemMessageContent "Html") (Just systemMessageTranslationContent)
|
||||||
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") (Just systemMessageTranslationSummary)
|
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") (Just systemMessageTranslationSummary)
|
||||||
)
|
)
|
||||||
<*> combinedButtonFieldF ""
|
<*> combinedButtonFieldF ""
|
||||||
|
|
||||||
@ -56,9 +57,9 @@ postMessageR cID = do
|
|||||||
((addTransRes, addTransView), addTransEnctype) <- runFormPost . identifyForm FIDSystemMessageAddTranslation . renderAForm FormStandard
|
((addTransRes, addTransView), addTransEnctype) <- runFormPost . identifyForm FIDSystemMessageAddTranslation . renderAForm FormStandard
|
||||||
$ SystemMessageTranslation
|
$ SystemMessageTranslation
|
||||||
<$> pure smId
|
<$> pure smId
|
||||||
<*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") Nothing
|
<*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) Nothing
|
||||||
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") Nothing
|
<*> areq htmlField' (fslpI MsgSystemMessageContent "Html") Nothing
|
||||||
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing
|
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") Nothing
|
||||||
|
|
||||||
formResult modifyRes $ modifySystemMessage smId
|
formResult modifyRes $ modifySystemMessage smId
|
||||||
|
|
||||||
@ -252,14 +253,15 @@ postMessageListR = do
|
|||||||
FormSuccess (_, _selection) -- prop> null _selection
|
FormSuccess (_, _selection) -- prop> null _selection
|
||||||
-> addMessageI Error MsgSystemMessageEmptySelection
|
-> addMessageI Error MsgSystemMessageEmptySelection
|
||||||
|
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
((addRes, addView), addEncoding) <- runFormPost . identifyForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage
|
((addRes, addView), addEncoding) <- runFormPost . identifyForm FIDSystemMessageAdd . renderAForm FormStandard $ SystemMessage
|
||||||
<$> aopt utcTimeField (fslI MsgSystemMessageFrom) Nothing
|
<$> aopt utcTimeField (fslI MsgSystemMessageFrom) Nothing
|
||||||
<*> aopt utcTimeField (fslI MsgSystemMessageTo) Nothing
|
<*> aopt utcTimeField (fslI MsgSystemMessageTo) Nothing
|
||||||
<*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) Nothing
|
<*> areq checkBoxField (fslI MsgSystemMessageAuthenticatedOnly) Nothing
|
||||||
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) Nothing
|
<*> areq (selectField optionsFinite) (fslI MsgSystemMessageSeverity) Nothing
|
||||||
<*> areq (langField False) (fslpI MsgSystemMessageLanguage "RFC1766-Sprachcode") (Just $ NonEmpty.head appLanguages)
|
<*> areq (langField False) (fslpI MsgSystemMessageLanguage (mr MsgRFC1766)) (Just $ NonEmpty.head appLanguages)
|
||||||
<*> areq htmlField' (fslpI MsgSystemMessageContent "HTML") Nothing
|
<*> areq htmlField' (fslpI MsgSystemMessageContent "Html") Nothing
|
||||||
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "HTML") Nothing
|
<*> aopt htmlField' (fslpI MsgSystemMessageSummary "Html") Nothing
|
||||||
|
|
||||||
case addRes of
|
case addRes of
|
||||||
FormMissing -> return ()
|
FormMissing -> return ()
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import qualified Data.Map as Map
|
|||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import qualified Control.Monad.State.Class as State
|
||||||
|
|
||||||
|
|
||||||
-- | Default start day of term for season,
|
-- | Default start day of term for season,
|
||||||
@ -18,32 +20,15 @@ defaultDay True Summer = fromGregorian 2020 4 1
|
|||||||
defaultDay False Summer = fromGregorian 2020 9 30
|
defaultDay False Summer = fromGregorian 2020 9 30
|
||||||
|
|
||||||
|
|
||||||
validateTerm :: Term -> [Text]
|
validateTerm :: (MonadHandler m, HandlerSite m ~ UniWorX)
|
||||||
validateTerm Term{..} =
|
=> FormValidator Term m ()
|
||||||
[ msg | (False, msg) <-
|
validateTerm = do
|
||||||
[ --startOk
|
Term{..} <- State.get
|
||||||
( termStart `withinTerm` termName
|
guardValidation MsgTermStartMustMatchName $ termStart `withinTerm` termName
|
||||||
, "Jahreszahl im Namenskürzel stimmt nicht mit Semesterbeginn überein."
|
guardValidation MsgTermEndMustBeAfterStart $ termStart < termEnd
|
||||||
)
|
guardValidation MsgTermLectureEndMustBeAfterStart $ termLectureStart < termLectureEnd
|
||||||
, -- endOk
|
guardValidation MsgTermStartMustBeBeforeLectureStart $ termStart <= termLectureStart
|
||||||
( termStart < termEnd
|
guardValidation MsgTermEndMustBeAfterLectureEnd $ termEnd >= termLectureEnd
|
||||||
, "Semester darf nicht enden, bevor es begann."
|
|
||||||
)
|
|
||||||
, -- startOk
|
|
||||||
( termLectureStart < termLectureEnd
|
|
||||||
, "Vorlesungszeit muss vor ihrem Ende anfgangen."
|
|
||||||
)
|
|
||||||
, -- lecStartOk
|
|
||||||
( termStart <= termLectureStart
|
|
||||||
, "Semester muss vor der Vorlesungszeit beginnen."
|
|
||||||
)
|
|
||||||
, -- lecEndOk
|
|
||||||
( termEnd >= termLectureEnd
|
|
||||||
, "Vorlesungszeit muss vor dem Semester enden."
|
|
||||||
)
|
|
||||||
] ]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
getTermShowR :: Handler TypedContent
|
getTermShowR :: Handler TypedContent
|
||||||
@ -66,22 +51,22 @@ getTermShowR = do
|
|||||||
provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
|
provideRep $ toJSON . map fst <$> runDB (E.select $ E.from termData)
|
||||||
provideRep $ do
|
provideRep $ do
|
||||||
let colonnadeTerms = widgetColonnade $ mconcat
|
let colonnadeTerms = widgetColonnade $ mconcat
|
||||||
[ sortable (Just "term-id") "Kürzel" $ \(Entity tid _, _) -> anchorCell
|
[ sortable (Just "term-id") (i18nCell MsgTermShort) $ \(Entity tid _, _) -> anchorCell
|
||||||
(TermCourseListR tid)
|
(TermCourseListR tid)
|
||||||
[whamlet|#{toPathPiece tid}|]
|
[whamlet|#{toPathPiece tid}|]
|
||||||
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
|
, sortable (Just "lecture-start") (i18nCell MsgLectureStart) $ \(Entity _ Term{..},_) ->
|
||||||
cell $ formatTime SelFormatDate termLectureStart >>= toWidget
|
cell $ formatTime SelFormatDate termLectureStart >>= toWidget
|
||||||
, sortable (Just "lecture-end") "Ende Vorlesungen" $ \(Entity _ Term{..},_) ->
|
, sortable (Just "lecture-end") (i18nCell MsgTermLectureEnd) $ \(Entity _ Term{..},_) ->
|
||||||
cell $ formatTime SelFormatDate termLectureEnd >>= toWidget
|
cell $ formatTime SelFormatDate termLectureEnd >>= toWidget
|
||||||
, sortable Nothing "Aktiv" $ \(Entity _ Term{..},_) ->
|
, sortable Nothing (i18nCell MsgTermActive) $ \(Entity _ Term{..},_) ->
|
||||||
tickmarkCell termActive
|
tickmarkCell termActive
|
||||||
, sortable Nothing "Kurse" $ \(_, E.Value numCourses) ->
|
, sortable Nothing (i18nCell MsgTermCourseCount) $ \(_, E.Value numCourses) ->
|
||||||
cell [whamlet|_{MsgNumCourses numCourses}|]
|
cell [whamlet|_{MsgNumCourses numCourses}|]
|
||||||
, sortable (Just "start") "Semesteranfang" $ \(Entity _ Term{..},_) ->
|
, sortable (Just "start") (i18nCell MsgTermStart) $ \(Entity _ Term{..},_) ->
|
||||||
cell $ formatTime SelFormatDate termStart >>= toWidget
|
cell $ formatTime SelFormatDate termStart >>= toWidget
|
||||||
, sortable (Just "end") "Semesterende" $ \(Entity _ Term{..},_) ->
|
, sortable (Just "end") (i18nCell MsgTermEnd) $ \(Entity _ Term{..},_) ->
|
||||||
cell $ formatTime SelFormatDate termEnd >>= toWidget
|
cell $ formatTime SelFormatDate termEnd >>= toWidget
|
||||||
, sortable Nothing "Feiertage im Semester" $ \(Entity _ Term{..},_) ->
|
, sortable Nothing (i18nCell MsgTermHolidays) $ \(Entity _ Term{..},_) ->
|
||||||
cell $ do
|
cell $ do
|
||||||
termHolidays' <- mapM (formatTime SelFormatDate) termHolidays
|
termHolidays' <- mapM (formatTime SelFormatDate) termHolidays
|
||||||
[whamlet|
|
[whamlet|
|
||||||
@ -248,7 +233,7 @@ termToTemplate (Just Term{..}) = TermFormTemplate
|
|||||||
}
|
}
|
||||||
|
|
||||||
newTermForm :: TermFormTemplate -> Form Term
|
newTermForm :: TermFormTemplate -> Form Term
|
||||||
newTermForm template html = do
|
newTermForm template = validateForm validateTerm $ \html -> do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
let
|
let
|
||||||
tidForm
|
tidForm
|
||||||
@ -264,7 +249,7 @@ newTermForm template html = do
|
|||||||
(fslI MsgTermHolidays & setTooltip MsgMassInputTip)
|
(fslI MsgTermHolidays & setTooltip MsgMassInputTip)
|
||||||
True
|
True
|
||||||
(tftHolidays template)
|
(tftHolidays template)
|
||||||
(result, widget) <- flip (renderAForm FormStandard) html $ Term
|
flip (renderAForm FormStandard) html $ Term
|
||||||
<$> tidForm
|
<$> tidForm
|
||||||
<*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template)
|
<*> areq dayField (fslI MsgTermStartDay & setTooltip MsgTermStartDayTooltip) (tftStart template)
|
||||||
<*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template)
|
<*> areq dayField (fslI MsgTermEndDay & setTooltip MsgTermEndDayTooltip) (tftEnd template)
|
||||||
@ -272,24 +257,3 @@ newTermForm template html = do
|
|||||||
<*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template)
|
<*> areq dayField (fslI MsgTermLectureStart) (tftLectureStart template)
|
||||||
<*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template)
|
<*> areq dayField (fslI MsgTermLectureEnd & setTooltip MsgTermLectureEndTooltip) (tftLectureEnd template)
|
||||||
<*> areq checkBoxField (fslI MsgTermActive) (tftActive template)
|
<*> areq checkBoxField (fslI MsgTermActive) (tftActive template)
|
||||||
return $ case result of
|
|
||||||
FormSuccess termResult
|
|
||||||
| errorMsgs <- validateTerm termResult
|
|
||||||
, not $ null errorMsgs ->
|
|
||||||
(FormFailure errorMsgs,
|
|
||||||
[whamlet|
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<div class="alert__content">
|
|
||||||
<h4> Fehler:
|
|
||||||
<ul>
|
|
||||||
$forall errmsg <- errorMsgs
|
|
||||||
<li> #{errmsg}
|
|
||||||
^{widget}
|
|
||||||
|]
|
|
||||||
)
|
|
||||||
_ -> (result, widget)
|
|
||||||
{-
|
|
||||||
where
|
|
||||||
set :: Text -> FieldSettings site
|
|
||||||
set = bfs
|
|
||||||
-}
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ tutorInvitationConfig = InvitationConfig{..}
|
|||||||
invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName
|
invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|]
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|]
|
||||||
invitationTokenConfig _ _ = do
|
invitationTokenConfig _ _ = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionTutor, ())
|
invitationForm _ _ _ = pure (JunctionTutor, ())
|
||||||
|
|||||||
@ -390,7 +390,7 @@ postAdminUserR uuid = do
|
|||||||
}
|
}
|
||||||
userDataWidget <- runDB $ makeProfileData $ Entity uid user
|
userDataWidget <- runDB $ makeProfileData $ Entity uid user
|
||||||
siteLayout heading $ do
|
siteLayout heading $ do
|
||||||
let deleteWidget = $(widgetFile "widgets/data-delete/data-delete")
|
let deleteWidget = $(i18nWidgetFile "data-delete")
|
||||||
$(widgetFile "adminUser")
|
$(widgetFile "adminUser")
|
||||||
|
|
||||||
|
|
||||||
@ -572,7 +572,7 @@ functionInvitationConfig = InvitationConfig{..}
|
|||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
return [ihamlet|_{SomeMessage $ MsgSchoolFunctionInviteExplanation (mr $ SomeMessage invTokenUserFunctionFunction)}|]
|
return [ihamlet|_{SomeMessage $ MsgSchoolFunctionInviteExplanation (mr $ SomeMessage invTokenUserFunctionFunction)}|]
|
||||||
invitationTokenConfig _ (InvDBDataUserFunction{..}, _) = do
|
invitationTokenConfig _ (InvDBDataUserFunction{..}, _) = do
|
||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
let itExpiresAt = Just $ Just invDBUserFunctionDeadline
|
let itExpiresAt = Just $ Just invDBUserFunctionDeadline
|
||||||
itAddAuth = Nothing
|
itAddAuth = Nothing
|
||||||
itStartsAt = Nothing
|
itStartsAt = Nothing
|
||||||
|
|||||||
@ -76,7 +76,7 @@ postAdminUserAddR = do
|
|||||||
, userWarningDays = userDefaultWarningDays
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userShowSex = userDefaultShowSex
|
, userShowSex = userDefaultShowSex
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userMailLanguages = def
|
, userLanguages = Nothing
|
||||||
, userCsvOptions = def
|
, userCsvOptions = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
|
|||||||
@ -90,7 +90,7 @@ formatTimeW :: (HasLocalTime t) => SelDateTimeFormat -> t -> Widget
|
|||||||
formatTimeW s t = toWidget =<< formatTime s t
|
formatTimeW s t = toWidget =<< formatTime s t
|
||||||
|
|
||||||
formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text
|
formatTimeMail :: (MonadMail m, HasLocalTime t) => SelDateTimeFormat -> t -> m Text
|
||||||
formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . mailLanguages <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t)
|
formatTimeMail sel t = fmap fromString $ Time.formatTime <$> (getTimeLocale' . view _Wrapped <$> askMailLanguages) <*> (unDateTimeFormat <$> askMailDateTimeFormat sel) <*> pure (toLocalTime t)
|
||||||
|
|
||||||
getTimeLocale :: MonadHandler m => m TimeLocale
|
getTimeLocale :: MonadHandler m => m TimeLocale
|
||||||
getTimeLocale = getTimeLocale' <$> languages
|
getTimeLocale = getTimeLocale' <$> languages
|
||||||
|
|||||||
@ -70,7 +70,7 @@ examBonus (Entity eId Exam{..}) = runConduit $
|
|||||||
[ E.when_
|
[ E.when_
|
||||||
( E.not_ . E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence )
|
( E.not_ . E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence )
|
||||||
E.then_
|
E.then_
|
||||||
( E.just (sheet E.^. SheetActiveTo) E.<=. examOccurrence E.?. ExamOccurrenceStart
|
( E.maybe E.true ((E.<=. examOccurrence E.?. ExamOccurrenceStart) . E.just) (sheet E.^. SheetActiveTo)
|
||||||
E.&&. sheet E.^. SheetVisibleFrom E.<=. examOccurrence E.?. ExamOccurrenceStart
|
E.&&. sheet E.^. SheetVisibleFrom E.<=. examOccurrence E.?. ExamOccurrenceStart
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@ -220,8 +220,17 @@ multiAction :: forall action a.
|
|||||||
-> FieldSettings UniWorX
|
-> FieldSettings UniWorX
|
||||||
-> Maybe action
|
-> Maybe action
|
||||||
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
|
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
|
||||||
multiAction acts fs@FieldSettings{..} defAction csrf = do
|
multiAction = multiAction' mpopt
|
||||||
(actionRes, actionView) <- mreq (selectField . optionsF $ Map.keysSet acts) fs defAction
|
|
||||||
|
multiAction' :: forall action a.
|
||||||
|
( RenderMessage UniWorX action, PathPiece action, Ord action )
|
||||||
|
=> (Field Handler action -> FieldSettings UniWorX -> Maybe action -> MForm Handler (FormResult action, FieldView UniWorX))
|
||||||
|
-> Map action (AForm Handler a)
|
||||||
|
-> FieldSettings UniWorX
|
||||||
|
-> Maybe action
|
||||||
|
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
|
||||||
|
multiAction' minp acts fs@FieldSettings{..} defAction csrf = do
|
||||||
|
(actionRes, actionView) <- minp (selectField . optionsF $ Map.keysSet acts) fs defAction
|
||||||
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
|
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
|
||||||
|
|
||||||
let actionResults = view _1 <$> results
|
let actionResults = view _1 <$> results
|
||||||
@ -284,21 +293,12 @@ htmlField' = htmlField
|
|||||||
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
natFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
natFieldI msg = convertField fromInteger toInteger $ checkBool (>= 0) msg $ intMinField 0
|
natFieldI msg = convertField fromInteger toInteger $ checkBool (>= 0) msg $ intMinField 0
|
||||||
|
|
||||||
natField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
|
||||||
natField d = convertField fromInteger toInteger $ checkBool (>= 0) (T.append d " muss eine natürliche Zahl sein.") $ intMinField 0
|
|
||||||
|
|
||||||
natIntField ::(Monad m, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m Integer
|
|
||||||
natIntField = natField
|
|
||||||
|
|
||||||
posIntField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage) => Text -> Field m i
|
|
||||||
posIntField d = convertField fromInteger toInteger $ checkBool (> 0) (T.append d " muss eine positive Zahl sein.") $ intMinField 1
|
|
||||||
|
|
||||||
posIntFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
posIntFieldI :: (Monad m, Integral i, RenderMessage (HandlerSite m) msg, RenderMessage (HandlerSite m) FormMessage) => msg -> Field m i
|
||||||
posIntFieldI msg = convertField fromInteger toInteger $ checkBool (> 0) msg $ intMinField 0
|
posIntFieldI msg = convertField fromInteger toInteger $ checkBool (> 0) msg $ intMinField 0
|
||||||
|
|
||||||
-- | Field to request integral number > 'm'
|
-- | Field to request integral number > 'm'
|
||||||
minIntField :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage) => i -> Text -> Field m i
|
minIntFieldI :: (Monad m, Integral i, Show i, RenderMessage (HandlerSite m) FormMessage, RenderMessage (HandlerSite m) msg) => i -> msg -> Field m i
|
||||||
minIntField m d = checkBool (> m) (T.concat [d," muss größer als ", T.pack $ show m, " sein."]) $ intMinField m
|
minIntFieldI m msg = checkBool (> m) msg $ intMinField m
|
||||||
|
|
||||||
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points
|
pointsField :: (Monad m, HandlerSite m ~ UniWorX) => Field m Points
|
||||||
pointsField = pointsFieldMinMax (Just 0) Nothing
|
pointsField = pointsFieldMinMax (Just 0) Nothing
|
||||||
@ -652,13 +652,17 @@ pseudonymWordField = checkMMap doCheck id $ ciField & addDatalist (return $ mkOp
|
|||||||
| otherwise
|
| otherwise
|
||||||
= return . Left $ MsgUnknownPseudonymWord (CI.original w)
|
= return . Left $ MsgUnknownPseudonymWord (CI.original w)
|
||||||
|
|
||||||
specificFileField :: UploadSpecificFile -> Field Handler (ConduitT () File Handler ())
|
|
||||||
|
type FileUploads = ConduitT () (Either FileId File) Handler ()
|
||||||
|
|
||||||
|
|
||||||
|
specificFileField :: UploadSpecificFile -> Field Handler FileUploads
|
||||||
specificFileField UploadSpecificFile{..} = Field{..}
|
specificFileField UploadSpecificFile{..} = Field{..}
|
||||||
where
|
where
|
||||||
fieldEnctype = Multipart
|
fieldEnctype = Multipart
|
||||||
fieldParse _ files
|
fieldParse _ files
|
||||||
| [f] <- files
|
| [f] <- files
|
||||||
= return . Right . Just $ yieldM (acceptFile f) .| modifyFileTitle (const $ unpack specificFileName)
|
= return . Right . Just $ yieldM (acceptFile f) .| modifyFileTitle (const $ unpack specificFileName) .| C.map Right
|
||||||
| null files = return $ Right Nothing
|
| null files = return $ Right Nothing
|
||||||
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
|
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
|
||||||
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/specificFileField")
|
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/specificFileField")
|
||||||
@ -670,14 +674,14 @@ specificFileField UploadSpecificFile{..} = Field{..}
|
|||||||
|
|
||||||
zipFileField :: Bool -- ^ Unpack zips?
|
zipFileField :: Bool -- ^ Unpack zips?
|
||||||
-> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions
|
-> Maybe (NonNull (Set Extension)) -- ^ Restrictions on file extensions
|
||||||
-> Field Handler (ConduitT () File Handler ())
|
-> Field Handler FileUploads
|
||||||
zipFileField doUnpack permittedExtensions = Field{..}
|
zipFileField doUnpack permittedExtensions = Field{..}
|
||||||
where
|
where
|
||||||
fieldEnctype = Multipart
|
fieldEnctype = Multipart
|
||||||
fieldParse _ files
|
fieldParse _ files
|
||||||
| [f@FileInfo{..}] <- files
|
| [f@FileInfo{..}] <- files
|
||||||
, maybe True (anyOf (re _nullable . folded . unpacked) ((flip isExtensionOf `on` CI.foldCase) $ unpack fileName)) permittedExtensions || doUnpack
|
, maybe True (anyOf (re _nullable . folded . unpacked) ((flip isExtensionOf `on` CI.foldCase) $ unpack fileName)) permittedExtensions || doUnpack
|
||||||
= return . Right . Just $ bool (yieldM . acceptFile) sourceFiles doUnpack f
|
= return . Right . Just $ bool (yieldM . fmap Right . acceptFile) ((.| C.map Right) . sourceFiles) doUnpack f
|
||||||
| null files = return $ Right Nothing
|
| null files = return $ Right Nothing
|
||||||
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
|
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
|
||||||
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField")
|
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField")
|
||||||
@ -689,7 +693,7 @@ zipFileField doUnpack permittedExtensions = Field{..}
|
|||||||
|
|
||||||
fileUploadForm :: Bool -- ^ Required?
|
fileUploadForm :: Bool -- ^ Required?
|
||||||
-> (Bool -> FieldSettings UniWorX) -- ^ given @unpackZips@ generate `FieldSettings` in the case of `UploadAny`
|
-> (Bool -> FieldSettings UniWorX) -- ^ given @unpackZips@ generate `FieldSettings` in the case of `UploadAny`
|
||||||
-> UploadMode -> AForm Handler (Maybe (ConduitT () File Handler ()))
|
-> UploadMode -> AForm Handler (Maybe FileUploads)
|
||||||
fileUploadForm isReq mkFs = \case
|
fileUploadForm isReq mkFs = \case
|
||||||
NoUpload
|
NoUpload
|
||||||
-> pure Nothing
|
-> pure Nothing
|
||||||
@ -698,21 +702,21 @@ fileUploadForm isReq mkFs = \case
|
|||||||
UploadSpecific{..}
|
UploadSpecific{..}
|
||||||
-> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles)
|
-> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles)
|
||||||
where
|
where
|
||||||
specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe (ConduitT () File Handler ()))
|
specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe FileUploads)
|
||||||
specificFileForm spec@UploadSpecificFile{..}
|
specificFileForm spec@UploadSpecificFile{..}
|
||||||
= bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing
|
= bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing
|
||||||
|
|
||||||
mergeFileSources :: [Maybe (ConduitT () File Handler ())] -> Maybe (ConduitT () File Handler ())
|
mergeFileSources :: [Maybe FileUploads] -> Maybe FileUploads
|
||||||
mergeFileSources (catMaybes -> sources) = case sources of
|
mergeFileSources (catMaybes -> sources) = case sources of
|
||||||
[] -> Nothing
|
[] -> Nothing
|
||||||
fs -> Just $ sequence_ fs
|
fs -> Just $ sequence_ fs
|
||||||
|
|
||||||
multiFileField' :: ConduitT () (Either FileId File) Handler () -- ^ Permitted files in same format as produced by `multiFileField`
|
multiFileField' :: FileUploads -- ^ Permitted files in same format as produced by `multiFileField`
|
||||||
-> Field Handler (ConduitT () (Either FileId File) Handler ())
|
-> Field Handler FileUploads
|
||||||
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton
|
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton
|
||||||
|
|
||||||
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference
|
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference
|
||||||
-> Field Handler (ConduitT () (Either FileId File) Handler ())
|
-> Field Handler FileUploads
|
||||||
multiFileField permittedFiles' = Field{..}
|
multiFileField permittedFiles' = Field{..}
|
||||||
where
|
where
|
||||||
fieldEnctype = Multipart
|
fieldEnctype = Multipart
|
||||||
@ -755,7 +759,7 @@ multiFileField permittedFiles' = Field{..}
|
|||||||
$(widgetFile "widgets/multiFileField")
|
$(widgetFile "widgets/multiFileField")
|
||||||
unpackZips :: Text
|
unpackZips :: Text
|
||||||
unpackZips = "unpack-zip"
|
unpackZips = "unpack-zip"
|
||||||
takeLefts :: Monad m => ConduitM (Either b a) b m ()
|
takeLefts :: Monad m => ConduitT (Either b a) b m ()
|
||||||
takeLefts = awaitForever $ \case
|
takeLefts = awaitForever $ \case
|
||||||
Right _ -> return ()
|
Right _ -> return ()
|
||||||
Left r -> yield r
|
Left r -> yield r
|
||||||
@ -831,7 +835,7 @@ sheetGroupAFormReq fs template = multiActionA selOptions fs (classify' <$> templ
|
|||||||
where
|
where
|
||||||
selOptions = Map.fromList
|
selOptions = Map.fromList
|
||||||
[ ( Arbitrary', Arbitrary
|
[ ( Arbitrary', Arbitrary
|
||||||
<$> apreq (natField "Gruppengröße") (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template)
|
<$> apreq (natFieldI MsgGroupSizeNotNatural) (fslI MsgSheetGroupMaxGroupsize & noValidate) (preview _maxParticipants =<< template)
|
||||||
)
|
)
|
||||||
, ( RegisteredGroups', pure RegisteredGroups )
|
, ( RegisteredGroups', pure RegisteredGroups )
|
||||||
, ( NoGroups', pure NoGroups )
|
, ( NoGroups', pure NoGroups )
|
||||||
@ -861,6 +865,10 @@ dayTimeField fs mutc = do
|
|||||||
| otherwise = (Nothing,Nothing)
|
| otherwise = (Nothing,Nothing)
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
fieldTimeFormat :: String
|
||||||
|
-- fieldTimeFormat = "%e.%m.%y %k:%M"
|
||||||
|
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S"
|
||||||
|
|
||||||
localTimeField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m LocalTime
|
localTimeField :: (MonadHandler m, HandlerSite m ~ UniWorX) => Field m LocalTime
|
||||||
localTimeField = Field
|
localTimeField = Field
|
||||||
{ fieldParse = parseHelperGen readTime
|
{ fieldParse = parseHelperGen readTime
|
||||||
@ -873,11 +881,7 @@ localTimeField = Field
|
|||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
fieldTimeFormat :: String
|
-- `defaultTimeLocale` is okay here, since `fieldTimeFormat` does not contain any words
|
||||||
--fieldTimeFormat = "%e.%m.%y %k:%M"
|
|
||||||
fieldTimeFormat = "%Y-%m-%dT%H:%M:%S"
|
|
||||||
|
|
||||||
-- `defaultTimeLocale` is okay here, since `fieldTimeFormat` does not contain any
|
|
||||||
readTime :: Text -> Either UniWorXMessage LocalTime
|
readTime :: Text -> Either UniWorXMessage LocalTime
|
||||||
readTime t =
|
readTime t =
|
||||||
case parseTimeM True defaultTimeLocale fieldTimeFormat (T.unpack t) of
|
case parseTimeM True defaultTimeLocale fieldTimeFormat (T.unpack t) of
|
||||||
@ -925,8 +929,8 @@ jsonField hide = Field{..}
|
|||||||
boolField :: ( MonadHandler m
|
boolField :: ( MonadHandler m
|
||||||
, HandlerSite m ~ UniWorX
|
, HandlerSite m ~ UniWorX
|
||||||
)
|
)
|
||||||
=> Field m Bool
|
=> Maybe (SomeMessage UniWorX) -> Field m Bool
|
||||||
boolField = Field
|
boolField mkNone = Field
|
||||||
{ fieldParse = \e _ -> return $ boolParser e
|
{ fieldParse = \e _ -> return $ boolParser e
|
||||||
, fieldView = \theId name attrs val isReq -> $(widgetFile "widgets/fields/bool")
|
, fieldView = \theId name attrs val isReq -> $(widgetFile "widgets/fields/bool")
|
||||||
, fieldEnctype = UrlEncoded
|
, fieldEnctype = UrlEncoded
|
||||||
@ -1150,7 +1154,7 @@ multiUserField onlySuggested suggestions = Field{..}
|
|||||||
case dbRes of
|
case dbRes of
|
||||||
[] -> return $ Left email
|
[] -> return $ Left email
|
||||||
[E.Value uid] -> return $ Right uid
|
[E.Value uid] -> return $ Right uid
|
||||||
_other -> throwE $ SomeMessage ("Ambiguous e-mail addr" :: Text)
|
_other -> throwE $ SomeMessage MsgAmbiguousEmail
|
||||||
|
|
||||||
examResultField :: forall m res.
|
examResultField :: forall m res.
|
||||||
( MonadHandler m
|
( MonadHandler m
|
||||||
@ -1244,13 +1248,14 @@ csvFormatOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs
|
|||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
let
|
let
|
||||||
opts =
|
opts =
|
||||||
[ (MsgCsvDelimiterNull, '\0')
|
[ (MsgCsvDelimiterNull, '\0')
|
||||||
, (MsgCsvDelimiterTab, '\t')
|
, (MsgCsvDelimiterTab, '\t')
|
||||||
, (MsgCsvDelimiterComma, ',')
|
, (MsgCsvDelimiterComma, ',')
|
||||||
, (MsgCsvDelimiterColon, chr 58)
|
, (MsgCsvDelimiterColon, chr 58)
|
||||||
, (MsgCsvDelimiterBar, '|')
|
, (MsgCsvDelimiterSemicolon, chr 59)
|
||||||
, (MsgCsvDelimiterSpace, ' ')
|
, (MsgCsvDelimiterBar, '|')
|
||||||
, (MsgCsvDelimiterUnitSep, chr 31)
|
, (MsgCsvDelimiterSpace, ' ')
|
||||||
|
, (MsgCsvDelimiterUnitSep, chr 31)
|
||||||
]
|
]
|
||||||
olReadExternal t = do
|
olReadExternal t = do
|
||||||
i <- readMay t
|
i <- readMay t
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
module Handler.Utils.I18n
|
module Handler.Utils.I18n
|
||||||
where
|
( i18nWidgetFile
|
||||||
|
, i18nWidgetFilesAvailable, i18nWidgetFilesAvailable', i18nWidgetFiles
|
||||||
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
import Language.Haskell.TH.Syntax (qRunIO)
|
import Language.Haskell.TH.Syntax (qRunIO)
|
||||||
|
import qualified Language.Haskell.TH.Syntax as TH
|
||||||
|
|
||||||
import qualified Data.List as List
|
import qualified Data.List as List
|
||||||
import qualified Data.List.NonEmpty as NonEmpty
|
import qualified Data.List.NonEmpty as NonEmpty
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
import System.Directory (listDirectory)
|
import System.Directory (listDirectory)
|
||||||
import System.FilePath.Posix (takeBaseName)
|
import System.FilePath.Posix (takeBaseName)
|
||||||
|
|
||||||
@ -40,4 +48,31 @@ i18nWidgetFile basename = do
|
|||||||
[ funD ws $ [ clause [litP $ stringL l] (normalB . widgetFile $ "i18n" </> basename </> l) []
|
[ funD ws $ [ clause [litP $ stringL l] (normalB . widgetFile $ "i18n" </> basename </> l) []
|
||||||
| l <- unpack <$> NonEmpty.toList availableTranslations' -- One function definition for every available language
|
| l <- unpack <$> NonEmpty.toList availableTranslations' -- One function definition for every available language
|
||||||
] ++ [ clause [wildP] (normalB [e| error "selectLanguage returned an invalid translation" |]) [] ] -- Fallback mostly there so compiler does not complain about non-exhaustive pattern match
|
] ++ [ clause [wildP] (normalB [e| error "selectLanguage returned an invalid translation" |]) [] ] -- Fallback mostly there so compiler does not complain about non-exhaustive pattern match
|
||||||
] [e|selectLanguage availableTranslations' >>= $(varE ws)|]
|
] [e|selectLanguage availableTranslations' >>= $(varE ws)|]
|
||||||
|
|
||||||
|
i18nWidgetFilesAvailable' :: FilePath -> Q (Map Text (NonEmpty Text))
|
||||||
|
i18nWidgetFilesAvailable' basename = do
|
||||||
|
let i18nDirectory = "templates" </> "i18n" </> basename
|
||||||
|
availableFiles <- qRunIO $ listDirectory i18nDirectory
|
||||||
|
let fileKinds' = fmap (pack . dropExtension . takeBaseName &&& toTranslation . pack . takeBaseName) availableFiles
|
||||||
|
fileKinds :: Map Text [Text]
|
||||||
|
fileKinds = sortWith (NTop . flip List.elemIndex (NonEmpty.toList appLanguages)) . Set.toList <$> Map.fromListWith Set.union [ (kind, Set.singleton l) | (kind, Just l) <- fileKinds' ]
|
||||||
|
toTranslation fName = listToMaybe . sortOn length . mapMaybe (flip Text.stripPrefix fName . (<>".")) $ map fst fileKinds'
|
||||||
|
|
||||||
|
iforM fileKinds $ \kind -> maybe (fail $ "‘" <> i18nDirectory <> "’ has no translations for ‘" <> unpack kind <> "’") return . NonEmpty.nonEmpty
|
||||||
|
|
||||||
|
i18nWidgetFilesAvailable :: FilePath -> Q Exp
|
||||||
|
i18nWidgetFilesAvailable = TH.lift <=< i18nWidgetFilesAvailable'
|
||||||
|
|
||||||
|
i18nWidgetFiles :: FilePath -> Q Exp
|
||||||
|
i18nWidgetFiles basename = do
|
||||||
|
availableTranslations' <- i18nWidgetFilesAvailable' basename
|
||||||
|
|
||||||
|
-- Dispatch to correct language (depending on user settings via `selectLanguage`) at run time
|
||||||
|
ws <- newName "ws" -- Name for dispatch function
|
||||||
|
letE
|
||||||
|
[ funD ws $ [ clause [litP $ stringL kind, litP $ stringL l] (normalB [e|$(widgetFile $ "i18n" </> basename </> kind <.> l) :: Widget|]) []
|
||||||
|
| (unpack -> kind, ls) <- Map.toList availableTranslations'
|
||||||
|
, l <- unpack <$> NonEmpty.toList ls
|
||||||
|
] ++ [ clause [wildP, wildP] (normalB [e| error "selectLanguage returned an invalid translation" |]) [] ] -- Fallback mostly there so compiler does not complain about non-exhaustive pattern match
|
||||||
|
] [e|imap (\kind ls -> selectLanguage ls >>= $(varE ws) kind) availableTranslations'|]
|
||||||
|
|||||||
@ -144,7 +144,7 @@ data InvitationConfig junction = forall formCtx. InvitationConfig
|
|||||||
|
|
||||||
-- | Additional configuration needed for an invocation of `bearerToken`
|
-- | Additional configuration needed for an invocation of `bearerToken`
|
||||||
data InvitationTokenConfig = InvitationTokenConfig
|
data InvitationTokenConfig = InvitationTokenConfig
|
||||||
{ itAuthority :: UserId
|
{ itAuthority :: Either Value UserId
|
||||||
, itAddAuth :: Maybe AuthDNF
|
, itAddAuth :: Maybe AuthDNF
|
||||||
, itExpiresAt :: Maybe (Maybe UTCTime)
|
, itExpiresAt :: Maybe (Maybe UTCTime)
|
||||||
, itStartsAt :: Maybe UTCTime
|
, itStartsAt :: Maybe UTCTime
|
||||||
|
|||||||
@ -47,14 +47,14 @@ userMailT :: ( MonadHandler m
|
|||||||
) => UserId -> MailT m a -> m a
|
) => UserId -> MailT m a -> m a
|
||||||
userMailT uid mAct = do
|
userMailT uid mAct = do
|
||||||
user@User
|
user@User
|
||||||
{ userMailLanguages
|
{ userLanguages
|
||||||
, userDateTimeFormat
|
, userDateTimeFormat
|
||||||
, userDateFormat
|
, userDateFormat
|
||||||
, userTimeFormat
|
, userTimeFormat
|
||||||
} <- liftHandler . runDB $ getJust uid
|
} <- liftHandler . runDB $ getJust uid
|
||||||
let
|
let
|
||||||
ctx = MailContext
|
ctx = MailContext
|
||||||
{ mcLanguages = userMailLanguages
|
{ mcLanguages = fromMaybe def userLanguages
|
||||||
, mcDateTimeFormat = \case
|
, mcDateTimeFormat = \case
|
||||||
SelFormatDateTime -> userDateTimeFormat
|
SelFormatDateTime -> userDateTimeFormat
|
||||||
SelFormatDate -> userDateFormat
|
SelFormatDate -> userDateFormat
|
||||||
|
|||||||
@ -47,8 +47,8 @@ instance Pretty x => Pretty (CI x) where
|
|||||||
|
|
||||||
|
|
||||||
instance Pretty SheetGrading where
|
instance Pretty SheetGrading where
|
||||||
pretty Points{..} = pretty ( show maxPoints <> " Punkte" :: String)
|
pretty Points{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e)" :: String)
|
||||||
pretty PassPoints{..} = pretty ( show maxPoints <> " Punkte, bestanden ab " <> show passingPoints <> " Punkte" :: String )
|
pretty PassPoints{..} = pretty ( "Maximal " <> show maxPoints <> " Punkt(e), bestanden ab " <> show passingPoints <> " Punkt(en)" :: String )
|
||||||
pretty PassBinary = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String )
|
pretty PassBinary = pretty ( "Bestanden (1) / Nicht bestanden (0)" :: String )
|
||||||
|
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ formatRating cID Rating{ ratingValues = Rating'{..}, ..} = let
|
|||||||
[ Just $ "Veranstaltung:" <+> pretty ratingCourseName
|
[ Just $ "Veranstaltung:" <+> pretty ratingCourseName
|
||||||
, Just $ "Blatt:" <+> pretty ratingSheetName
|
, Just $ "Blatt:" <+> pretty ratingSheetName
|
||||||
, ("Korrektor:" <+>) . pretty <$> ratingCorrectorName
|
, ("Korrektor:" <+>) . pretty <$> ratingCorrectorName
|
||||||
, ("Bewertung:" <+>) . pretty <$> (ratingSheetType ^? _grading)
|
, ("Bewertungsschema:" <+>) . pretty <$> (ratingSheetType ^? _grading)
|
||||||
]
|
]
|
||||||
, "Abgabe-Id:" <+> pretty (Text.unpack $ toPathPiece cID)
|
, "Abgabe-Id:" <+> pretty (Text.unpack $ toPathPiece cID)
|
||||||
, "============================================="
|
, "============================================="
|
||||||
|
|||||||
11
src/Handler/Utils/Routes.hs
Normal file
11
src/Handler/Utils/Routes.hs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module Handler.Utils.Routes
|
||||||
|
( classifyHandler
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Utils.TH.Routes
|
||||||
|
|
||||||
|
|
||||||
|
classifyHandler :: Route UniWorX -> String
|
||||||
|
classifyHandler = $(classifyHandler' uniworxRoutes)
|
||||||
@ -10,7 +10,7 @@ import qualified Database.Esqueleto.Internal.Sql as E
|
|||||||
-- | Map sheet file types to their visibily dates of a given sheet, for convenience
|
-- | Map sheet file types to their visibily dates of a given sheet, for convenience
|
||||||
sheetFileTypeDates :: Sheet -> SheetFileType -> Maybe UTCTime
|
sheetFileTypeDates :: Sheet -> SheetFileType -> Maybe UTCTime
|
||||||
sheetFileTypeDates Sheet{..} = \case
|
sheetFileTypeDates Sheet{..} = \case
|
||||||
SheetExercise -> Just sheetActiveFrom
|
SheetExercise -> sheetActiveFrom
|
||||||
SheetHint -> sheetHintFrom
|
SheetHint -> sheetHintFrom
|
||||||
SheetSolution -> sheetSolutionFrom
|
SheetSolution -> sheetSolutionFrom
|
||||||
SheetMarking -> Nothing
|
SheetMarking -> Nothing
|
||||||
|
|||||||
@ -50,6 +50,8 @@ import Text.Hamlet (ihamletFile)
|
|||||||
|
|
||||||
import qualified Control.Monad.Catch as E (Handler(..))
|
import qualified Control.Monad.Catch as E (Handler(..))
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
data AssignSubmissionException = NoCorrectors
|
data AssignSubmissionException = NoCorrectors
|
||||||
| NoCorrectorsByProportion
|
| NoCorrectorsByProportion
|
||||||
| SubmissionsNotFound (NonNull (Set SubmissionId))
|
| SubmissionsNotFound (NonNull (Set SubmissionId))
|
||||||
@ -428,7 +430,7 @@ sinkSubmission userId mExists isUpdate = do
|
|||||||
, Just (map unpack . Set.toList . toNullable -> exts) <- extensionRestriction
|
, Just (map unpack . Set.toList . toNullable -> exts) <- extensionRestriction
|
||||||
= Conduit.mapM $ \x -> if
|
= Conduit.mapM $ \x -> if
|
||||||
| Left File{..} <- x
|
| Left File{..} <- x
|
||||||
, none (`isExtensionOf` fileTitle) exts
|
, none ((flip isExtensionOf `on` CI.foldCase) fileTitle) exts
|
||||||
, isn't _Nothing fileContent -- File record is not a directory, we don't care about those
|
, isn't _Nothing fileContent -- File record is not a directory, we don't care about those
|
||||||
-> throwM $ InvalidFileTitleExtension fileTitle
|
-> throwM $ InvalidFileTitleExtension fileTitle
|
||||||
| otherwise
|
| otherwise
|
||||||
|
|||||||
@ -203,7 +203,7 @@ fltrAllocationActive cTime queryAllocation = singletonMap "active" . FilterColum
|
|||||||
E.&&. E.maybe E.true (\t -> E.val cTime E.<=. t) (allocation E.^. AllocationRegisterTo)
|
E.&&. E.maybe E.true (\t -> E.val cTime E.<=. t) (allocation E.^. AllocationRegisterTo)
|
||||||
|
|
||||||
fltrAllocationActiveUI :: DBFilterUI
|
fltrAllocationActiveUI :: DBFilterUI
|
||||||
fltrAllocationActiveUI mPrev = prismAForm (singletonFilter "active" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgAllocationActive)
|
fltrAllocationActiveUI mPrev = prismAForm (singletonFilter "active" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgAllocationActive)
|
||||||
|
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
@ -355,7 +355,7 @@ fltrApplicationVeto :: OpticFilterColumn t Bool
|
|||||||
fltrApplicationVeto queryVeto = singletonMap "veto" . FilterColumn . mkExactFilter $ view queryVeto
|
fltrApplicationVeto queryVeto = singletonMap "veto" . FilterColumn . mkExactFilter $ view queryVeto
|
||||||
|
|
||||||
fltrApplicationVetoUI :: DBFilterUI
|
fltrApplicationVetoUI :: DBFilterUI
|
||||||
fltrApplicationVetoUI mPrev = prismAForm (singletonFilter "veto" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgCourseApplicationVeto)
|
fltrApplicationVetoUI mPrev = prismAForm (singletonFilter "veto" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseApplicationVeto)
|
||||||
|
|
||||||
colApplicationRatingComment :: OpticColonnade (Maybe Text)
|
colApplicationRatingComment :: OpticColonnade (Maybe Text)
|
||||||
colApplicationRatingComment resultComment = Colonnade.singleton (fromSortable header) body
|
colApplicationRatingComment resultComment = Colonnade.singleton (fromSortable header) body
|
||||||
@ -407,7 +407,7 @@ fltrApplicationFiles :: OpticFilterColumn t Bool
|
|||||||
fltrApplicationFiles queryFiles = singletonMap "has-files" . FilterColumn . mkExactFilter $ view queryFiles
|
fltrApplicationFiles queryFiles = singletonMap "has-files" . FilterColumn . mkExactFilter $ view queryFiles
|
||||||
|
|
||||||
fltrApplicationFilesUI :: DBFilterUI
|
fltrApplicationFilesUI :: DBFilterUI
|
||||||
fltrApplicationFilesUI mPrev = prismAForm (singletonFilter "has-files" . maybePrism _PathPiece) mPrev $ aopt boolField (fslI MsgCourseApplicationFiles)
|
fltrApplicationFilesUI mPrev = prismAForm (singletonFilter "has-files" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgCourseApplicationFiles)
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
-- Files
|
-- Files
|
||||||
|
|||||||
@ -142,6 +142,7 @@ import Text.Blaze.Instances as Import ()
|
|||||||
import Jose.Jwt.Instances as Import ()
|
import Jose.Jwt.Instances as Import ()
|
||||||
import Web.PathPieces.Instances as Import ()
|
import Web.PathPieces.Instances as Import ()
|
||||||
import Data.Universe.Instances.Reverse.MonoTraversable ()
|
import Data.Universe.Instances.Reverse.MonoTraversable ()
|
||||||
|
import Data.Universe.Instances.Reverse.WithIndex ()
|
||||||
import Database.Persist.Class.Instances as Import ()
|
import Database.Persist.Class.Instances as Import ()
|
||||||
import Database.Persist.Types.Instances as Import ()
|
import Database.Persist.Types.Instances as Import ()
|
||||||
import Data.UUID.Instances as Import ()
|
import Data.UUID.Instances as Import ()
|
||||||
@ -152,6 +153,9 @@ import Crypto.Hash.Instances as Import ()
|
|||||||
import Colonnade.Instances as Import ()
|
import Colonnade.Instances as Import ()
|
||||||
import Data.Bool.Instances as Import ()
|
import Data.Bool.Instances as Import ()
|
||||||
import Data.Encoding.Instances as Import ()
|
import Data.Encoding.Instances as Import ()
|
||||||
|
import Prometheus.Instances as Import ()
|
||||||
|
|
||||||
|
import Crypto.Hash as Import (Digest, SHA3_256)
|
||||||
|
|
||||||
import Control.Lens as Import
|
import Control.Lens as Import
|
||||||
hiding ( (<.>)
|
hiding ( (<.>)
|
||||||
|
|||||||
62
src/Jobs.hs
62
src/Jobs.hs
@ -71,6 +71,7 @@ import Jobs.Handler.TransactionLog
|
|||||||
import Jobs.Handler.SynchroniseLdap
|
import Jobs.Handler.SynchroniseLdap
|
||||||
import Jobs.Handler.PruneInvitations
|
import Jobs.Handler.PruneInvitations
|
||||||
import Jobs.Handler.ChangeUserDisplayEmail
|
import Jobs.Handler.ChangeUserDisplayEmail
|
||||||
|
import Jobs.Handler.PruneFiles
|
||||||
|
|
||||||
import Jobs.HealthReport
|
import Jobs.HealthReport
|
||||||
|
|
||||||
@ -250,23 +251,26 @@ execCrontab :: RWST JobContext () (HashMap JobCtl (Max UTCTime)) (HandlerFor Uni
|
|||||||
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
|
-- ^ Keeping a `HashMap` of the latest execution times of `JobCtl`s we have
|
||||||
-- seen, wait for the time of the next job and fire it
|
-- seen, wait for the time of the next job and fire it
|
||||||
execCrontab = do
|
execCrontab = do
|
||||||
mapRWST (liftHandler . runDB . setSerializable) $ do
|
let
|
||||||
let
|
mergeState :: MonadResource m => RWST _ () _ (ReaderT SqlBackend m) ()
|
||||||
mergeLastExec (Entity _leId CronLastExec{..})
|
mergeState = do
|
||||||
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
let
|
||||||
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
|
mergeLastExec (Entity _leId CronLastExec{..})
|
||||||
| otherwise = return ()
|
| Just job <- Aeson.parseMaybe parseJSON cronLastExecJob
|
||||||
|
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max cronLastExecTime)
|
||||||
|
| otherwise = return ()
|
||||||
|
|
||||||
mergeQueued (Entity _qjId QueuedJob{..})
|
mergeQueued (Entity _qjId QueuedJob{..})
|
||||||
| Just job <- Aeson.parseMaybe parseJSON queuedJobContent
|
| Just job <- Aeson.parseMaybe parseJSON queuedJobContent
|
||||||
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max queuedJobCreationTime)
|
= State.modify $ HashMap.insertWith (<>) (JobCtlQueue job) (Max queuedJobCreationTime)
|
||||||
| otherwise = return ()
|
| otherwise = return ()
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeLastExec
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeLastExec
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeQueued
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ mergeQueued
|
||||||
|
mapRWST (liftHandler . runDB . setSerializable) $ mergeState
|
||||||
|
|
||||||
refT <- liftIO getCurrentTime
|
refT <- liftIO getCurrentTime
|
||||||
settings <- getsYesod appSettings'
|
settings <- getsYesod appSettings'
|
||||||
(currentCrontab, (jobCtl, nextMatch)) <- mapRWST (liftIO . atomically) $ do
|
(currentCrontab, (jobCtl, nextMatch), currentState) <- mapRWST (liftIO . atomically) $ do
|
||||||
crontab <- liftBase . readTVar =<< asks jobCrontab
|
crontab <- liftBase . readTVar =<< asks jobCrontab
|
||||||
|
|
||||||
State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
|
State.modify . HashMap.filterWithKey $ \k _ -> HashMap.member k crontab
|
||||||
@ -274,7 +278,7 @@ execCrontab = do
|
|||||||
case earliestJob settings prevExec crontab refT of
|
case earliestJob settings prevExec crontab refT of
|
||||||
Nothing -> liftBase retry
|
Nothing -> liftBase retry
|
||||||
Just (_, MatchNone) -> liftBase retry
|
Just (_, MatchNone) -> liftBase retry
|
||||||
Just x -> return (crontab, x)
|
Just x -> return (crontab, x, prevExec)
|
||||||
|
|
||||||
do
|
do
|
||||||
lastTimes <- State.get
|
lastTimes <- State.get
|
||||||
@ -283,18 +287,24 @@ execCrontab = do
|
|||||||
|
|
||||||
let doJob = mapRWST (liftHandler . runDBJobs . setSerializable) $ do
|
let doJob = mapRWST (liftHandler . runDBJobs . setSerializable) $ do
|
||||||
newCrontab <- lift . hoist lift $ determineCrontab'
|
newCrontab <- lift . hoist lift $ determineCrontab'
|
||||||
if
|
when (newCrontab /= currentCrontab) $
|
||||||
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
|
mapRWST (liftIO . atomically) $
|
||||||
-> do
|
liftBase . void . flip swapTVar newCrontab =<< asks jobCrontab
|
||||||
now <- liftIO $ getCurrentTime
|
|
||||||
foundation <- getYesod
|
mergeState
|
||||||
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
|
newState <- State.get
|
||||||
case jobCtl of
|
|
||||||
JobCtlQueue job -> lift $ queueDBJobCron job
|
let upToDate = and
|
||||||
other -> runReaderT ?? foundation $ writeJobCtl other
|
[ ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
|
||||||
| otherwise
|
, ((==) `on` HashMap.lookup jobCtl) newState currentState
|
||||||
-> mapRWST (liftIO . atomically) $
|
]
|
||||||
liftBase . void . flip swapTVar newCrontab =<< asks jobCrontab
|
when upToDate $ do
|
||||||
|
now <- liftIO $ getCurrentTime
|
||||||
|
foundation <- getYesod
|
||||||
|
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
|
||||||
|
case jobCtl of
|
||||||
|
JobCtlQueue job -> lift $ queueDBJobCron job
|
||||||
|
other -> runReaderT ?? foundation $ writeJobCtl other
|
||||||
|
|
||||||
case nextMatch of
|
case nextMatch of
|
||||||
MatchAsap -> doJob
|
MatchAsap -> doJob
|
||||||
|
|||||||
@ -49,6 +49,15 @@ determineCrontab = execWriterT $ do
|
|||||||
, cronRateLimit = appJobCronInterval
|
, cronRateLimit = appJobCronInterval
|
||||||
, cronNotAfter = Right CronNotScheduled
|
, cronNotAfter = Right CronNotScheduled
|
||||||
}
|
}
|
||||||
|
whenIsJust appPruneUnreferencedFiles $ \pInterval ->
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue JobPruneUnreferencedFiles)
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronAsap
|
||||||
|
, cronRepeat = CronRepeatScheduled CronAsap
|
||||||
|
, cronRateLimit = pInterval
|
||||||
|
, cronNotAfter = Right CronNotScheduled
|
||||||
|
}
|
||||||
|
|
||||||
oldestInvitationMUTC <- lift $ preview (_head . _entityVal . _invitationExpiresAt . _Just) <$> selectList [InvitationExpiresAt !=. Nothing] [Asc InvitationExpiresAt, LimitTo 1]
|
oldestInvitationMUTC <- lift $ preview (_head . _entityVal . _invitationExpiresAt . _Just) <$> selectList [InvitationExpiresAt !=. Nothing] [Asc InvitationExpiresAt, LimitTo 1]
|
||||||
whenIsJust oldestInvitationMUTC $ \oldestInvUTC -> tell $ HashMap.singleton
|
whenIsJust oldestInvitationMUTC $ \oldestInvUTC -> tell $ HashMap.singleton
|
||||||
@ -60,6 +69,16 @@ determineCrontab = execWriterT $ do
|
|||||||
, cronNotAfter = Right CronNotScheduled
|
, cronNotAfter = Right CronNotScheduled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
oldestSessionFile <- lift $ preview (_head . _entityVal . _sessionFileTouched) <$> selectList [] [Asc SessionFileTouched, LimitTo 1]
|
||||||
|
whenIsJust oldestSessionFile $ \oldest -> tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue JobPruneSessionFiles)
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp . utcToLocalTime $ addUTCTime appSessionFilesExpire oldest
|
||||||
|
, cronRepeat = CronRepeatOnChange
|
||||||
|
, cronRateLimit = appSessionFilesExpire / 2
|
||||||
|
, cronNotAfter = Right CronNotScheduled
|
||||||
|
}
|
||||||
|
|
||||||
tell . flip foldMap universeF $ \kind ->
|
tell . flip foldMap universeF $ \kind ->
|
||||||
case appHealthCheckInterval kind of
|
case appHealthCheckInterval kind of
|
||||||
Just int -> HashMap.singleton
|
Just int -> HashMap.singleton
|
||||||
@ -144,39 +163,41 @@ determineCrontab = execWriterT $ do
|
|||||||
|
|
||||||
let
|
let
|
||||||
sheetJobs (Entity nSheet Sheet{..}) = do
|
sheetJobs (Entity nSheet Sheet{..}) = do
|
||||||
tell $ HashMap.singleton
|
for_ sheetActiveFrom $ \aFrom ->
|
||||||
(JobCtlQueue $ JobQueueNotification NotificationSheetActive{..})
|
|
||||||
Cron
|
|
||||||
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveFrom
|
|
||||||
, cronRepeat = CronRepeatNever
|
|
||||||
, cronRateLimit = appNotificationRateLimit
|
|
||||||
, cronNotAfter = Right . CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveTo
|
|
||||||
}
|
|
||||||
tell $ HashMap.singleton
|
|
||||||
(JobCtlQueue $ JobQueueNotification NotificationSheetSoonInactive{..})
|
|
||||||
Cron
|
|
||||||
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ . max sheetActiveFrom $ addUTCTime (-nominalDay) sheetActiveTo
|
|
||||||
, cronRepeat = CronRepeatOnChange -- Allow repetition of the notification (if something changes), but wait at least an hour
|
|
||||||
, cronRateLimit = appNotificationRateLimit
|
|
||||||
, cronNotAfter = Right . CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveTo
|
|
||||||
}
|
|
||||||
tell $ HashMap.singleton
|
|
||||||
(JobCtlQueue $ JobQueueNotification NotificationSheetInactive{..})
|
|
||||||
Cron
|
|
||||||
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveTo
|
|
||||||
, cronRepeat = CronRepeatOnChange
|
|
||||||
, cronRateLimit = appNotificationRateLimit
|
|
||||||
, cronNotAfter = Left appNotificationExpiration
|
|
||||||
}
|
|
||||||
when sheetAutoDistribute $
|
|
||||||
tell $ HashMap.singleton
|
tell $ HashMap.singleton
|
||||||
(JobCtlQueue $ JobDistributeCorrections nSheet)
|
(JobCtlQueue $ JobQueueNotification NotificationSheetActive{..})
|
||||||
Cron
|
Cron
|
||||||
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ sheetActiveTo
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ aFrom
|
||||||
, cronRepeat = CronRepeatNever
|
, cronRepeat = CronRepeatNever
|
||||||
, cronRateLimit = 3600 -- Irrelevant due to `cronRepeat`
|
, cronRateLimit = appNotificationRateLimit
|
||||||
, cronNotAfter = Left nominalDay
|
, cronNotAfter = Right $ maybe CronNotScheduled (CronTimestamp . utcToLocalTimeTZ appTZ) sheetActiveTo
|
||||||
}
|
}
|
||||||
|
for_ sheetActiveTo $ \aTo -> do
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue $ JobQueueNotification NotificationSheetSoonInactive{..})
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ . maybe id max sheetActiveFrom $ addUTCTime (-nominalDay) aTo
|
||||||
|
, cronRepeat = CronRepeatOnChange -- Allow repetition of the notification (if something changes), but wait at least an hour
|
||||||
|
, cronRateLimit = appNotificationRateLimit
|
||||||
|
, cronNotAfter = Right . CronTimestamp $ utcToLocalTimeTZ appTZ aTo
|
||||||
|
}
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue $ JobQueueNotification NotificationSheetInactive{..})
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ aTo
|
||||||
|
, cronRepeat = CronRepeatOnChange
|
||||||
|
, cronRateLimit = appNotificationRateLimit
|
||||||
|
, cronNotAfter = Left appNotificationExpiration
|
||||||
|
}
|
||||||
|
when sheetAutoDistribute $
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue $ JobDistributeCorrections nSheet)
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp $ utcToLocalTimeTZ appTZ aTo
|
||||||
|
, cronRepeat = CronRepeatNever
|
||||||
|
, cronRateLimit = 3600 -- Irrelevant due to `cronRepeat`
|
||||||
|
, cronNotAfter = Left nominalDay
|
||||||
|
}
|
||||||
|
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ sheetJobs
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ sheetJobs
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import Text.Hamlet
|
|||||||
|
|
||||||
dispatchJobChangeUserDisplayEmail :: UserId -> UserEmail -> Handler ()
|
dispatchJobChangeUserDisplayEmail :: UserId -> UserEmail -> Handler ()
|
||||||
dispatchJobChangeUserDisplayEmail jUser jDisplayEmail = do
|
dispatchJobChangeUserDisplayEmail jUser jDisplayEmail = do
|
||||||
token <- tokenRestrict SetDisplayEmailR jDisplayEmail <$> bearerToken jUser (Just $ HashSet.singleton SetDisplayEmailR) Nothing Nothing Nothing
|
token <- tokenRestrict SetDisplayEmailR jDisplayEmail <$> bearerToken (Right jUser) (Just $ HashSet.singleton SetDisplayEmailR) Nothing Nothing Nothing
|
||||||
jwt <- encodeToken token
|
jwt <- encodeToken token
|
||||||
let
|
let
|
||||||
setDisplayEmailUrl :: SomeRoute UniWorX
|
setDisplayEmailUrl :: SomeRoute UniWorX
|
||||||
|
|||||||
38
src/Jobs/Handler/PruneFiles.hs
Normal file
38
src/Jobs/Handler/PruneFiles.hs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
module Jobs.Handler.PruneFiles
|
||||||
|
( dispatchJobPruneSessionFiles
|
||||||
|
, dispatchJobPruneUnreferencedFiles
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import hiding (matching)
|
||||||
|
|
||||||
|
import Database.Persist.Sql (deleteWhereCount)
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
|
||||||
|
dispatchJobPruneSessionFiles :: Handler ()
|
||||||
|
dispatchJobPruneSessionFiles = do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
expires <- getsYesod $ view _appSessionFilesExpire
|
||||||
|
n <- runDB $ deleteWhereCount [ SessionFileTouched <. addUTCTime (- expires) now ]
|
||||||
|
$logInfoS "PruneSessionFiles" [st|Deleted #{n} expired session files|]
|
||||||
|
|
||||||
|
|
||||||
|
dispatchJobPruneUnreferencedFiles :: Handler ()
|
||||||
|
dispatchJobPruneUnreferencedFiles = do
|
||||||
|
n <- runDB . E.deleteCount . E.from $ \file ->
|
||||||
|
E.where_ . E.not_ . E.any E.exists $ references file
|
||||||
|
$logInfoS "PruneUnreferencedFiles" [st|Deleted #{n} unreferenced files|]
|
||||||
|
where
|
||||||
|
references :: E.SqlExpr (Entity File) -> [E.SqlQuery ()]
|
||||||
|
references ((E.^. FileId) -> fId) =
|
||||||
|
[ E.from $ \matching -> E.where_ $ matching E.^. AllocationMatchingLog E.==. fId
|
||||||
|
, E.from $ \appInstr -> E.where_ $ appInstr E.^. CourseAppInstructionFileFile E.==. fId
|
||||||
|
, E.from $ \appFile -> E.where_ $ appFile E.^. CourseApplicationFileFile E.==. fId
|
||||||
|
, E.from $ \matFile -> E.where_ $ matFile E.^. MaterialFileFile E.==. fId
|
||||||
|
, E.from $ \newsFile -> E.where_ $ newsFile E.^. CourseNewsFileFile E.==. fId
|
||||||
|
, E.from $ \sessFile -> E.where_ $ sessFile E.^. SessionFileFile E.==. fId
|
||||||
|
, E.from $ \sheetFile -> E.where_ $ sheetFile E.^. SheetFileFile E.==. fId
|
||||||
|
, E.from $ \subFile -> E.where_ $ subFile E.^. SubmissionFileFile E.==. fId
|
||||||
|
]
|
||||||
@ -22,4 +22,10 @@ import Jobs.Handler.SendNotification.CourseRegistered
|
|||||||
|
|
||||||
|
|
||||||
dispatchJobSendNotification :: UserId -> Notification -> Handler ()
|
dispatchJobSendNotification :: UserId -> Notification -> Handler ()
|
||||||
dispatchJobSendNotification jRecipient jNotification = $(dispatchTH ''Notification) jNotification jRecipient
|
dispatchJobSendNotification jRecipient jNotification = do
|
||||||
|
$(dispatchTH ''Notification) jNotification jRecipient
|
||||||
|
|
||||||
|
instanceID' <- getsYesod $ view instanceID
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
|
runDB . insert_ $ SentNotification (toJSON jNotification) jRecipient now instanceID'
|
||||||
|
|||||||
@ -16,7 +16,7 @@ ihamletSomeMessage f trans = f $ trans . SomeMessage
|
|||||||
mkEditNotifications :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
mkEditNotifications :: (MonadHandler m, HandlerSite m ~ UniWorX) => UserId -> m (HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
mkEditNotifications uid = liftHandler $ do
|
mkEditNotifications uid = liftHandler $ do
|
||||||
cID <- encrypt uid
|
cID <- encrypt uid
|
||||||
jwt <- encodeToken =<< bearerToken uid (Just . HashSet.singleton $ UserNotificationR cID) Nothing Nothing Nothing
|
jwt <- encodeToken =<< bearerToken (Right uid) (Just . HashSet.singleton $ UserNotificationR cID) Nothing Nothing Nothing
|
||||||
let
|
let
|
||||||
editNotificationsUrl :: SomeRoute UniWorX
|
editNotificationsUrl :: SomeRoute UniWorX
|
||||||
editNotificationsUrl = SomeRoute (UserNotificationR cID, [(toPathPiece GetBearer, toPathPiece jwt)])
|
editNotificationsUrl = SomeRoute (UserNotificationR cID, [(toPathPiece GetBearer, toPathPiece jwt)])
|
||||||
|
|||||||
@ -29,7 +29,7 @@ dispatchJobSendPasswordReset jRecipient = userMailT jRecipient $ do
|
|||||||
LTUUnique utc' _ -> utc'
|
LTUUnique utc' _ -> utc'
|
||||||
_other -> UTCTime (addDays 2 $ utctDay now) 0
|
_other -> UTCTime (addDays 2 $ utctDay now) 0
|
||||||
|
|
||||||
resetToken' <- bearerToken jRecipient (Just . HashSet.singleton $ UserPasswordR cID) Nothing (Just $ Just tomorrowEndOfDay) Nothing
|
resetToken' <- bearerToken (Right jRecipient) (Just . HashSet.singleton $ UserPasswordR cID) Nothing (Just $ Just tomorrowEndOfDay) Nothing
|
||||||
let resetToken = resetToken'
|
let resetToken = resetToken'
|
||||||
& tokenRestrict (UserPasswordR cID) (decodeUtf8 . Base64.encode . BA.convert $ computeUserAuthenticationDigest userAuthentication)
|
& tokenRestrict (UserPasswordR cID) (decodeUtf8 . Base64.encode . BA.convert $ computeUserAuthenticationDigest userAuthentication)
|
||||||
encodedToken <- encodeToken resetToken
|
encodedToken <- encodeToken resetToken
|
||||||
|
|||||||
@ -67,6 +67,8 @@ data Job = JobSendNotification { jRecipient :: UserId, jNotification :: Notifica
|
|||||||
| JobChangeUserDisplayEmail { jUser :: UserId
|
| JobChangeUserDisplayEmail { jUser :: UserId
|
||||||
, jDisplayEmail :: UserEmail
|
, jDisplayEmail :: UserEmail
|
||||||
}
|
}
|
||||||
|
| JobPruneSessionFiles
|
||||||
|
| JobPruneUnreferencedFiles
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
||||||
| NotificationSheetActive { nSheet :: SheetId }
|
| NotificationSheetActive { nSheet :: SheetId }
|
||||||
|
|||||||
46
src/Mail.hs
46
src/Mail.hs
@ -10,7 +10,7 @@ module Mail
|
|||||||
, MailT, defMailT
|
, MailT, defMailT
|
||||||
, MailSmtpData(..)
|
, MailSmtpData(..)
|
||||||
, _MailSmtpDataSet
|
, _MailSmtpDataSet
|
||||||
, MailContext(..), MailLanguages(..)
|
, MailContext(..)
|
||||||
, MonadMail(..)
|
, MonadMail(..)
|
||||||
, getMailMessageRender, getMailMsgRenderer
|
, getMailMessageRender, getMailMsgRenderer
|
||||||
-- * YesodMail
|
-- * YesodMail
|
||||||
@ -38,7 +38,7 @@ module Mail
|
|||||||
|
|
||||||
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON)
|
import ClassyPrelude.Yesod hiding (snoc, (.=), getMessageRender, derivePersistFieldJSON)
|
||||||
|
|
||||||
import Model.Types.TH.JSON
|
import Model.Types.Languages
|
||||||
|
|
||||||
import Network.Mail.Mime hiding (addPart, addAttachment)
|
import Network.Mail.Mime hiding (addPart, addAttachment)
|
||||||
import qualified Network.Mail.Mime as Mime (addPart)
|
import qualified Network.Mail.Mime as Mime (addPart)
|
||||||
@ -89,7 +89,7 @@ import qualified Data.Binary as Binary
|
|||||||
import "network-bsd" Network.BSD (getHostName)
|
import "network-bsd" Network.BSD (getHostName)
|
||||||
|
|
||||||
import Data.Time.Zones (TZ, utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
|
import Data.Time.Zones (TZ, utcTZ, utcToLocalTimeTZ, timeZoneForUTCTime)
|
||||||
import Data.Time.LocalTime (ZonedTime(..))
|
import Data.Time.LocalTime (ZonedTime(..), TimeZone(..))
|
||||||
import Data.Time.Format (rfc822DateFormat)
|
import Data.Time.Format (rfc822DateFormat)
|
||||||
|
|
||||||
import Network.HaskellNet.SMTP (SMTPConnection)
|
import Network.HaskellNet.SMTP (SMTPConnection)
|
||||||
@ -109,8 +109,6 @@ import Data.Universe.Instances.Reverse ()
|
|||||||
import Data.Universe.Instances.Reverse.JSON ()
|
import Data.Universe.Instances.Reverse.JSON ()
|
||||||
import Data.Universe.Instances.Reverse.Hashable ()
|
import Data.Universe.Instances.Reverse.Hashable ()
|
||||||
|
|
||||||
import GHC.Exts (IsList)
|
|
||||||
|
|
||||||
import Control.Monad.Trans.Maybe (MaybeT(..))
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||||
|
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
@ -160,19 +158,8 @@ _MailSmtpDataSet = to $ \MailSmtpData{..} -> none id
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
newtype MailLanguages = MailLanguages { mailLanguages :: [Lang] }
|
|
||||||
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
|
||||||
deriving newtype (FromJSON, ToJSON, IsList)
|
|
||||||
|
|
||||||
instance Default MailLanguages where
|
|
||||||
def = MailLanguages []
|
|
||||||
|
|
||||||
instance Hashable MailLanguages
|
|
||||||
instance NFData MailLanguages
|
|
||||||
|
|
||||||
|
|
||||||
data MailContext = MailContext
|
data MailContext = MailContext
|
||||||
{ mcLanguages :: MailLanguages
|
{ mcLanguages :: Languages
|
||||||
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
|
, mcDateTimeFormat :: SelDateTimeFormat -> DateTimeFormat
|
||||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
@ -191,7 +178,7 @@ instance Default MailContext where
|
|||||||
makeLenses_ ''MailContext
|
makeLenses_ ''MailContext
|
||||||
|
|
||||||
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
class (MonadHandler m, MonadState Mail m) => MonadMail m where
|
||||||
askMailLanguages :: m MailLanguages
|
askMailLanguages :: m Languages
|
||||||
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
|
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
|
||||||
tellMailSmtpData :: MailSmtpData -> m ()
|
tellMailSmtpData :: MailSmtpData -> m ()
|
||||||
|
|
||||||
@ -214,7 +201,7 @@ getMailMessageRender :: ( MonadMail m
|
|||||||
, HandlerSite m ~ site
|
, HandlerSite m ~ site
|
||||||
, RenderMessage site msg
|
, RenderMessage site msg
|
||||||
) => m (msg -> Text)
|
) => m (msg -> Text)
|
||||||
getMailMessageRender = renderMessage <$> getYesod <*> (mailLanguages <$> askMailLanguages)
|
getMailMessageRender = renderMessage <$> getYesod <*> (view _Wrapped <$> askMailLanguages)
|
||||||
|
|
||||||
getMailMsgRenderer :: forall site m.
|
getMailMsgRenderer :: forall site m.
|
||||||
( MonadMail m
|
( MonadMail m
|
||||||
@ -515,8 +502,24 @@ setDateCurrent = setDate =<< liftIO getCurrentTime
|
|||||||
setDate :: (MonadHandler m, YesodMail (HandlerSite m)) => UTCTime -> MailT m ()
|
setDate :: (MonadHandler m, YesodMail (HandlerSite m)) => UTCTime -> MailT m ()
|
||||||
setDate time = do
|
setDate time = do
|
||||||
tz <- mailDateTZ
|
tz <- mailDateTZ
|
||||||
let timeStr = formatTime defaultTimeLocale rfc822DateFormat $ ZonedTime (utcToLocalTimeTZ tz time) (timeZoneForUTCTime tz time)
|
let timeStr = formatTime defaultTimeLocale rfc822DateFormat $ ZonedTime (utcToLocalTimeTZ tz time) (rfc822zone $ timeZoneForUTCTime tz time)
|
||||||
replaceMailHeader "Date" . Just $ pack timeStr
|
replaceMailHeader "Date" . Just $ pack timeStr
|
||||||
|
where
|
||||||
|
rfc822zone tz'
|
||||||
|
| tz' `elem` rfc822zones = tz'
|
||||||
|
| otherwise = tz' { timeZoneName = "" }
|
||||||
|
rfc822zones =
|
||||||
|
[ TimeZone 0 False "UT"
|
||||||
|
, TimeZone 0 False "GMT"
|
||||||
|
, TimeZone (-5 * 60) False "EST"
|
||||||
|
, TimeZone (-4 * 60) True "EDT"
|
||||||
|
, TimeZone (-6 * 60) False "CST"
|
||||||
|
, TimeZone (-5 * 60) True "CDT"
|
||||||
|
, TimeZone (-7 * 60) False "MST"
|
||||||
|
, TimeZone (-6 * 60) True "MDT"
|
||||||
|
, TimeZone (-8 * 60) False "PST"
|
||||||
|
, TimeZone (-7 * 60) True "PDT"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m ()
|
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m ()
|
||||||
@ -543,6 +546,3 @@ setMailSmtpData = do
|
|||||||
in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
|
in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
|
||||||
| otherwise
|
| otherwise
|
||||||
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }
|
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }
|
||||||
|
|
||||||
|
|
||||||
derivePersistFieldJSON ''MailLanguages
|
|
||||||
|
|||||||
@ -577,6 +577,14 @@ customMigrations = Map.fromListWith (>>)
|
|||||||
ALTER TABLE "allocation" DROP COLUMN "matching_log";
|
ALTER TABLE "allocation" DROP COLUMN "matching_log";
|
||||||
|]
|
|]
|
||||||
)
|
)
|
||||||
|
, ( AppliedMigrationKey [migrationVersion|26.0.0|] [version|27.0.0|]
|
||||||
|
, whenM (tableExists "user") $
|
||||||
|
[executeQQ|
|
||||||
|
ALTER TABLE "user" ADD COLUMN "languages" jsonb;
|
||||||
|
UPDATE "user" SET "languages" = "mail_languages" where "mail_languages" <> '[]';
|
||||||
|
ALTER TABLE "user" DROP COLUMN "mail_languages";
|
||||||
|
|]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ import Data.Binary (Binary)
|
|||||||
data BearerToken site = BearerToken
|
data BearerToken site = BearerToken
|
||||||
{ tokenIdentifier :: TokenId
|
{ tokenIdentifier :: TokenId
|
||||||
-- ^ Unique identifier for each token; maybe useful for tracing usage of tokens
|
-- ^ Unique identifier for each token; maybe useful for tracing usage of tokens
|
||||||
, tokenAuthority :: AuthId site
|
, tokenAuthority :: Either Value (AuthId site)
|
||||||
-- ^ Tokens only grant rights the `tokenAuthority` has (i.e. `AuthTag`s are evaluated with the user set to `tokenAuthority`)
|
-- ^ Tokens only grant rights the `tokenAuthority` has (i.e. `AuthTag`s are evaluated with the user set to `tokenAuthority`)
|
||||||
, tokenRoutes :: Maybe (HashSet (Route site))
|
, tokenRoutes :: Maybe (HashSet (Route site))
|
||||||
-- ^ Tokens can optionally be restricted to only be usable on a subset of routes
|
-- ^ Tokens can optionally be restricted to only be usable on a subset of routes
|
||||||
@ -97,7 +97,7 @@ tokenToJSON :: forall m.
|
|||||||
--
|
--
|
||||||
-- Monadic context is needed because `AuthId`s are encrypted during encoding
|
-- Monadic context is needed because `AuthId`s are encrypted during encoding
|
||||||
tokenToJSON BearerToken{..} = do
|
tokenToJSON BearerToken{..} = do
|
||||||
cID <- encrypt tokenAuthority :: m (CryptoUUID (AuthId (HandlerSite m)))
|
cID <- either (return . Left) (fmap Right . encrypt) tokenAuthority :: m (Either Value (CryptoUUID (AuthId (HandlerSite m))))
|
||||||
let stdPayload = Jose.JwtClaims
|
let stdPayload = Jose.JwtClaims
|
||||||
{ jwtIss = Just $ toPathPiece tokenIssuedBy
|
{ jwtIss = Just $ toPathPiece tokenIssuedBy
|
||||||
, jwtSub = Nothing
|
, jwtSub = Nothing
|
||||||
@ -108,7 +108,7 @@ tokenToJSON BearerToken{..} = do
|
|||||||
, jwtJti = Just $ toPathPiece tokenIdentifier
|
, jwtJti = Just $ toPathPiece tokenIdentifier
|
||||||
}
|
}
|
||||||
return . JSON.object $
|
return . JSON.object $
|
||||||
catMaybes [ Just $ "authority" .= cID
|
catMaybes [ Just $ "authority" .= either id toJSON cID
|
||||||
, ("routes" .=) <$> tokenRoutes
|
, ("routes" .=) <$> tokenRoutes
|
||||||
, ("add-auth" .=) <$> tokenAddAuth
|
, ("add-auth" .=) <$> tokenAddAuth
|
||||||
, ("restrictions" .=) <$> assertM' (not . HashMap.null) tokenRestrictions
|
, ("restrictions" .=) <$> assertM' (not . HashMap.null) tokenRestrictions
|
||||||
@ -128,8 +128,8 @@ tokenParseJSON :: forall site.
|
|||||||
--
|
--
|
||||||
-- It's usually easier to use `Utils.Tokens.tokenParseJSON'`
|
-- It's usually easier to use `Utils.Tokens.tokenParseJSON'`
|
||||||
tokenParseJSON v@(Object o) = do
|
tokenParseJSON v@(Object o) = do
|
||||||
tokenAuthority' <- lift (o .: "authority") :: ReaderT CryptoIDKey Parser (CryptoUUID (AuthId site))
|
tokenAuthority' <- lift $ (Right <$> o .: "authority") <|> (Left <$> o .: "authority") :: ReaderT CryptoIDKey Parser (Either Value (CryptoUUID (AuthId site)))
|
||||||
tokenAuthority <- decrypt tokenAuthority'
|
tokenAuthority <- either (return . Left) (fmap Right . decrypt) tokenAuthority'
|
||||||
|
|
||||||
tokenRoutes <- lift $ o .:? "routes"
|
tokenRoutes <- lift $ o .:? "routes"
|
||||||
tokenAddAuth <- lift $ o .:? "add-auth"
|
tokenAddAuth <- lift $ o .:? "add-auth"
|
||||||
|
|||||||
@ -14,3 +14,4 @@ import Model.Types.Submission as Types
|
|||||||
import Model.Types.Misc as Types
|
import Model.Types.Misc as Types
|
||||||
import Model.Types.School as Types
|
import Model.Types.School as Types
|
||||||
import Model.Types.Allocation as Types
|
import Model.Types.Allocation as Types
|
||||||
|
import Model.Types.Languages as Types
|
||||||
|
|||||||
@ -52,3 +52,5 @@ type InstanceId = UUID
|
|||||||
type ClusterId = UUID
|
type ClusterId = UUID
|
||||||
type TokenId = UUID
|
type TokenId = UUID
|
||||||
type TermCandidateIncidence = UUID
|
type TermCandidateIncidence = UUID
|
||||||
|
|
||||||
|
type SessionFileReference = Digest SHA3_256
|
||||||
|
|||||||
25
src/Model/Types/Languages.hs
Normal file
25
src/Model/Types/Languages.hs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving, UndecidableInstances #-}
|
||||||
|
|
||||||
|
module Model.Types.Languages
|
||||||
|
( Languages(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON)
|
||||||
|
import GHC.Exts (IsList)
|
||||||
|
|
||||||
|
import Model.Types.TH.JSON
|
||||||
|
|
||||||
|
import Control.Lens.TH (makeWrapped)
|
||||||
|
|
||||||
|
|
||||||
|
newtype Languages = Languages [Lang]
|
||||||
|
deriving (Eq, Ord, Show, Read, Generic, Typeable)
|
||||||
|
deriving newtype (FromJSON, ToJSON, IsList)
|
||||||
|
|
||||||
|
instance Default Languages where
|
||||||
|
def = Languages []
|
||||||
|
|
||||||
|
instance Hashable Languages
|
||||||
|
instance NFData Languages
|
||||||
|
derivePersistFieldJSON ''Languages
|
||||||
|
makeWrapped ''Languages
|
||||||
@ -21,6 +21,11 @@ import qualified Data.Aeson.Types as Aeson
|
|||||||
|
|
||||||
import qualified Data.Binary as Binary
|
import qualified Data.Binary as Binary
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import Model.Types.TH.PathPiece
|
||||||
|
import Database.Persist.Sql
|
||||||
|
|
||||||
|
|
||||||
data AuthenticationMode = AuthLDAP
|
data AuthenticationMode = AuthLDAP
|
||||||
| AuthPWHash { authPWHash :: Text }
|
| AuthPWHash { authPWHash :: Text }
|
||||||
@ -152,3 +157,21 @@ instance (Ord a, Binary a) => Binary (PredDNF a) where
|
|||||||
type AuthLiteral = PredLiteral AuthTag
|
type AuthLiteral = PredLiteral AuthTag
|
||||||
|
|
||||||
type AuthDNF = PredDNF AuthTag
|
type AuthDNF = PredDNF AuthTag
|
||||||
|
|
||||||
|
|
||||||
|
data UserGroupName
|
||||||
|
= UserGroupMetrics
|
||||||
|
| UserGroupCustom { userGroupCustomName :: CI Text }
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
instance PathPiece UserGroupName where
|
||||||
|
toPathPiece UserGroupMetrics = "metrics"
|
||||||
|
toPathPiece (UserGroupCustom t) = CI.original t
|
||||||
|
fromPathPiece t = Just $ if
|
||||||
|
| "metrics" `ciEq` t -> UserGroupMetrics
|
||||||
|
| otherwise -> UserGroupCustom $ CI.mk t
|
||||||
|
where
|
||||||
|
ciEq = (==) `on` CI.mk
|
||||||
|
|
||||||
|
pathPieceJSON ''UserGroupName
|
||||||
|
derivePersistFieldPathPiece' (sqlType (Proxy @(CI Text))) ''UserGroupName
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
module Model.Types.TH.PathPiece
|
module Model.Types.TH.PathPiece
|
||||||
( derivePersistFieldPathPiece
|
( derivePersistFieldPathPiece
|
||||||
|
, derivePersistFieldPathPiece'
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude.Yesod
|
import ClassyPrelude.Yesod
|
||||||
@ -13,7 +14,10 @@ import Language.Haskell.TH.Datatype
|
|||||||
|
|
||||||
|
|
||||||
derivePersistFieldPathPiece :: Name -> DecsQ
|
derivePersistFieldPathPiece :: Name -> DecsQ
|
||||||
derivePersistFieldPathPiece tName = do
|
derivePersistFieldPathPiece = derivePersistFieldPathPiece' SqlString
|
||||||
|
|
||||||
|
derivePersistFieldPathPiece' :: SqlType -> Name -> DecsQ
|
||||||
|
derivePersistFieldPathPiece' sType tName = do
|
||||||
DatatypeInfo{..} <- reifyDatatype tName
|
DatatypeInfo{..} <- reifyDatatype tName
|
||||||
vars <- forM datatypeVars (const $ newName "a")
|
vars <- forM datatypeVars (const $ newName "a")
|
||||||
let t = foldl (\t' n' -> t' `appT` varT n') (conT tName) vars
|
let t = foldl (\t' n' -> t' `appT` varT n') (conT tName) vars
|
||||||
@ -32,15 +36,18 @@ derivePersistFieldPathPiece tName = do
|
|||||||
[ do
|
[ do
|
||||||
bs <- newName "bs"
|
bs <- newName "bs"
|
||||||
clause [[p|PersistByteString $(varP bs)|]] (normalB [e|maybe (Left "Could not decode PathPiece from PersistByteString") Right $ fromPathPiece =<< either (const Nothing) Just (Text.decodeUtf8' $(varE bs))|]) []
|
clause [[p|PersistByteString $(varP bs)|]] (normalB [e|maybe (Left "Could not decode PathPiece from PersistByteString") Right $ fromPathPiece =<< either (const Nothing) Just (Text.decodeUtf8' $(varE bs))|]) []
|
||||||
|
, do
|
||||||
|
bs <- newName "bs"
|
||||||
|
clause [[p|PersistDbSpecific $(varP bs)|]] (normalB [e|maybe (Left "Could not decode PathPiece from PersistDbSpecific") Right $ fromPathPiece =<< either (const Nothing) Just (Text.decodeUtf8' $(varE bs))|]) []
|
||||||
, do
|
, do
|
||||||
text <- newName "text"
|
text <- newName "text"
|
||||||
clause [[p|PersistText $(varP text)|]] (normalB [e|maybe (Left "Could not decode PathPiece from PersistTetx") Right $ fromPathPiece $(varE text)|]) []
|
clause [[p|PersistText $(varP text)|]] (normalB [e|maybe (Left "Could not decode PathPiece from PersistText") Right $ fromPathPiece $(varE text)|]) []
|
||||||
, clause [wildP] (normalB [e|Left "PathPiece values must be converted from PersistText or PersistByteString"|]) []
|
, clause [wildP] (normalB [e|Left "PathPiece values must be converted from PersistText, PersistByteString, or PersistDbSpecific"|]) []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, instanceD sqlCxt ([t|PersistFieldSql|] `appT` t)
|
, instanceD sqlCxt ([t|PersistFieldSql|] `appT` t)
|
||||||
[ funD 'sqlType
|
[ funD 'sqlType
|
||||||
[ clause [wildP] (normalB [e|SqlString|]) []
|
[ clause [wildP] (normalB [e|sType|]) []
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
29
src/Prometheus/Instances.hs
Normal file
29
src/Prometheus/Instances.hs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Prometheus.Instances
|
||||||
|
() where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
import Prometheus
|
||||||
|
import Data.Aeson
|
||||||
|
|
||||||
|
import qualified Data.Map.Strict as Map
|
||||||
|
|
||||||
|
|
||||||
|
instance ToJSON SampleType where
|
||||||
|
toJSON = String . tshow
|
||||||
|
|
||||||
|
instance ToJSON SampleGroup where
|
||||||
|
toJSON (SampleGroup Info{..} sgType samples) = object
|
||||||
|
[ "name" .= metricName
|
||||||
|
, "help" .= metricHelp
|
||||||
|
, "type" .= sgType
|
||||||
|
, "metrics" .= samples
|
||||||
|
]
|
||||||
|
|
||||||
|
instance ToJSON Sample where
|
||||||
|
toJSON (Sample sName sLabels sValue) = object
|
||||||
|
[ "name" .= sName
|
||||||
|
, "labels" .= Map.fromList sLabels
|
||||||
|
, "value" .= decodeUtf8 sValue
|
||||||
|
]
|
||||||
@ -126,6 +126,9 @@ data AppSettings = AppSettings
|
|||||||
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
|
, appSynchroniseLdapUsersWithin :: Maybe NominalDiffTime
|
||||||
, appSynchroniseLdapUsersInterval :: NominalDiffTime
|
, appSynchroniseLdapUsersInterval :: NominalDiffTime
|
||||||
|
|
||||||
|
, appSessionFilesExpire :: NominalDiffTime
|
||||||
|
, appPruneUnreferencedFiles :: Maybe NominalDiffTime
|
||||||
|
|
||||||
, appInitialLogSettings :: LogSettings
|
, appInitialLogSettings :: LogSettings
|
||||||
|
|
||||||
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
, appTransactionLogIPRetentionTime :: NominalDiffTime
|
||||||
@ -417,6 +420,9 @@ instance FromJSON AppSettings where
|
|||||||
appSynchroniseLdapUsersWithin <- o .:? "synchronise-ldap-users-within"
|
appSynchroniseLdapUsersWithin <- o .:? "synchronise-ldap-users-within"
|
||||||
appSynchroniseLdapUsersInterval <- o .: "synchronise-ldap-users-interval"
|
appSynchroniseLdapUsersInterval <- o .: "synchronise-ldap-users-interval"
|
||||||
|
|
||||||
|
appSessionFilesExpire <- o .: "session-files-expire"
|
||||||
|
appPruneUnreferencedFiles <- o .:? "prune-unreferenced-files"
|
||||||
|
|
||||||
appMaximumContentLength <- o .: "maximum-content-length"
|
appMaximumContentLength <- o .: "maximum-content-length"
|
||||||
|
|
||||||
appReloadTemplates <- o .:? "reload-templates" .!= defaultDev
|
appReloadTemplates <- o .:? "reload-templates" .!= defaultDev
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user