Merge branch 'master' into 476-interface-fur-klausurkorrekturen

This commit is contained in:
Sarah Vaupel 2020-01-07 10:28:33 +01:00
commit f619b0a36c
439 changed files with 21059 additions and 15893 deletions

View File

@ -1,4 +0,0 @@
((haskell-mode . ((haskell-indent-spaces . 4)
(haskell-process-use-ghci . t)))
(hamlet-mode . ((hamlet/basic-offset . 4)
(haskell-process-use-ghci . t))))

6
.gitignore vendored
View File

@ -1,7 +1,4 @@
dist*
static/bundles/
static/tmp/
static/combined/
node_modules/
*.hi
*.o
@ -36,3 +33,6 @@ tags
test.log
*.dump-splices
/.stack-work.lock
/.npmrc
/config/webpack.yml
static/wp-*/

248
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,248 @@
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:
- ./.npmrc.gup
- npm install
before_script: &npm
- apt-get update -y
- npm install -g n
- n stable
- npm install -g npm
- hash -r
- apt-get install openssh-client -y
- install -v -m 0700 -d ~/.ssh
- install -v -T -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts
- install -v -T -m 0400 ${SSH_DEPLOY_KEY} ~/.ssh/deploy && echo "IdentityFile ~/.ssh/deploy" >> ~/.ssh/config;
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
- config/webpack.yml
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++
- apt-get install openssh-client -y
- install -v -m 0700 -d ~/.ssh
- install -v -T -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts
- install -v -T -m 0400 ${SSH_DEPLOY_KEY} ~/.ssh/deploy && echo "IdentityFile ~/.ssh/deploy" >> ~/.ssh/config;
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++
- apt-get install -y --no-install-recommends openssh-client
- install -v -m 0700 -d ~/.ssh
- install -v -T -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts
- install -v -T -m 0400 ${SSH_DEPLOY_KEY} ~/.ssh/deploy && echo "IdentityFile ~/.ssh/deploy" >> ~/.ssh/config;
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 -v -m 0700 -d ~/.ssh
- install -v -T -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts
- install -v -T -m 0400 ${SSH_PRIVATE_KEY_UNIWORX3} ~/.ssh/uniworx3; echo "IdentityFile ~/.ssh/uniworx3" >> ~/.ssh/config;
dependencies:
- yesod:build
only:
variables:
- $CI_COMMIT_REF_NAME =~ /^v[0-9].*/

12
.npmrc.gup Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
if command -V gup 1>&- 2>&-; then
gup --always
fi
cat >${1:-.npmrc} <<EOF
@fortawesome:registry=https://npm.fontawesome.com/
//npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN}
EOF

File diff suppressed because it is too large Load Diff

View File

@ -4,39 +4,39 @@ set -e
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
case $1 in
"")
exec -- stack clean
;;
*)
target=".stack-work-${1}"
shift
if [[ -n "${1}" ]]; then
target=".stack-work-${1}"
else
target=".stack-work"
fi
shift
if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1
fi
if [[ -e .stack-work-clean ]]; then
printf ".stack-work-clean exists\n" >&2
exit 1
fi
if [[ ! -d "${target}" ]]; then
printf "%s does not exist or is no directory\n" "${target}" >&2
exit 1
fi
move-back() {
if [[ -d .stack-work ]]; then
mv -v .stack-work "${target}"
else
mkdir -v "${target}"
fi
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
}
if [[ "${target}" != ".stack-work" ]]; then
if [[ -e .stack-work-clean ]]; then
printf ".stack-work-clean exists\n" >&2
exit 1
fi
mv -v .stack-work .stack-work-clean
mv -v "${target}" .stack-work
trap move-back EXIT
move-back() {
if [[ -d .stack-work ]]; then
mv -v .stack-work "${target}"
else
mkdir -v "${target}"
fi
[[ -d .stack-work-clean ]] && mv -v .stack-work-clean .stack-work
}
(
set -ex
stack clean $@
)
;;
esac
mv -v .stack-work .stack-work-clean
mv -v "${target}" .stack-work
trap move-back EXIT
fi
(
set -ex
stack clean $@
)

View File

@ -706,6 +706,8 @@ model/x3d+xml x3dz x3d
text/cache-manifest manifest appcache
text/calendar ifb ics
text/css less css
text/x-sass sass
text/x-scss scss
text/csv csv
text/html shtml html htm
text/mathml mml

View File

@ -4,6 +4,7 @@
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
static-dir: "_env:STATIC_DIR:static"
webpack-manifest: "_env:WEBPACK_MANIFEST:config/webpack.yml"
host: "_env:HOST:*4" # any IPv4 host
port: "_env:PORT:3000"
ip-from-header: "_env:IP_FROM_HEADER:false"
@ -30,6 +31,8 @@ session-timeout: 7200
jwt-expiration: 604800
jwt-encoding: HS256
maximum-content-length: "_env:MAX_UPLOAD_SIZE:134217728"
session-files-expire: 3600
prune-unreferenced-files: 86400
health-check-interval:
matching-cluster-config: "_env:HEALTHCHECK_INTERVAL_MATCHING_CLUSTER_CONFIG:600"
http-reachable: "_env:HEALTHCHECK_INTERVAL_HTTP_REACHABLE:600"
@ -130,6 +133,7 @@ user-defaults:
time-format: "%R"
download-files: false
warning-days: 1209600
show-sex: false
# During central allocations lecturer-given ratings of applications (as
# ExamGrades) are combined with a central priority.

View File

@ -0,0 +1,9 @@
@use "~@fortawesome/fontawesome-pro/scss/fontawesome" with ( $fa-font-path: "~@fortawesome/fontawesome-pro/webfonts" )
@forward "~@fortawesome/fontawesome-pro/scss/fontawesome"
@use "~@fortawesome/fontawesome-pro/scss/solid"
@use "~typeface-roboto" as roboto
@use "~typeface-source-sans-pro" as source-sans-pro

View File

@ -4,8 +4,7 @@ import { I18n } from './services/i18n/i18n';
import { UtilRegistry } from './services/util-registry/util-registry';
import { isValidUtility } from './core/utility';
// load window.fetch polyfill
import 'whatwg-fetch';
import './app.sass';
export class App {
httpClient = new HttpClient();

1200
frontend/src/app.sass Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
/* eslint-env node */
import { App } from './app';
import { Utility } from './core/utility';
@ -13,52 +15,50 @@ const TEST_UTILS = [
];
describe('App', () => {
let app;
beforeEach(() => {
app = new App();
global.App = new App();
});
it('should create', () => {
expect(app).toBeTruthy();
expect(global.App).toBeTruthy();
});
it('should setup all utlites when page is done loading', () => {
spyOn(app.utilRegistry, 'setupAll');
spyOn(global.App.utilRegistry, 'setupAll');
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(app.utilRegistry.setupAll).toHaveBeenCalled();
expect(global.App.utilRegistry.setupAll).toHaveBeenCalled();
});
describe('provides services', () => {
it('HttpClient as httpClient', () => {
expect(app.httpClient).toBeTruthy();
expect(global.App.httpClient).toBeTruthy();
});
it('HtmlHelpers as htmlHelpers', () => {
expect(app.htmlHelpers).toBeTruthy();
expect(global.App.htmlHelpers).toBeTruthy();
});
it('I18n as i18n', () => {
expect(app.i18n).toBeTruthy();
expect(global.App.i18n).toBeTruthy();
});
it('UtilRegistry as utilRegistry', () => {
expect(app.utilRegistry).toBeTruthy();
expect(global.App.utilRegistry).toBeTruthy();
});
});
describe('registerUtilities()', () => {
it('should register the given utilities', () => {
spyOn(app.utilRegistry, 'register');
app.registerUtilities(TEST_UTILS);
expect(app.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length);
expect(app.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]);
expect(app.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]);
spyOn(global.App.utilRegistry, 'register');
global.App.registerUtilities(TEST_UTILS);
expect(global.App.utilRegistry.register.calls.count()).toBe(TEST_UTILS.length);
expect(global.App.utilRegistry.register.calls.argsFor(0)).toEqual([TEST_UTILS[0]]);
expect(global.App.utilRegistry.register.calls.argsFor(1)).toEqual([TEST_UTILS[1]]);
});
it('should throw an error if not passed an array of utilities', () => {
expect(() => {
app.registerUtilities({});
global.App.registerUtilities({});
}).toThrow();
});
});

View File

@ -0,0 +1,223 @@
/* global global:writable */
import * as semver from 'semver';
export const LOCATION = {
LOCAL: 'local',
WINDOW: 'window',
};
const LOCATION_SHADOWING = [ LOCATION.WINDOW, LOCATION.LOCAL ];
export class StorageManager {
namespace;
version;
_options;
_global;
constructor(namespace, version, options) {
this.namespace = namespace;
this.version = semver.valid(version);
if (!namespace) {
throw new Error('Cannot setup StorageManager without namespace');
}
if (!this.version) {
throw new Error('Cannot setup StorageManager without valid semver version');
}
if (options !== undefined) {
this._options = options;
}
if (global !== undefined)
this._global = global;
else if (window !== undefined)
this._global = window;
else
throw new Error('Cannot setup StorageManager without window or global');
}
save(key, value, options=this._options) {
if (!key) {
throw new Error('StorageManager.save called with invalid key');
}
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.save called with unsupported location option');
}
const location = options && options.location !== undefined ? options.location : LOCATION_SHADOWING[0];
switch (location) {
case LOCATION.LOCAL: {
this._saveToLocalStorage({ ...this._getFromLocalStorage(), [key]: value });
break;
}
case LOCATION.WINDOW: {
this._saveToWindow({ ...this._getFromLocalStorage(), [key]: value });
break;
}
default:
console.error('StorageManager.save cannot save item with unsupported location');
}
}
load(key, options=this._options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.load called with unsupported location option');
}
let locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
while (locations.length > 0) {
const location = locations.shift();
let val;
switch (location) {
case LOCATION.LOCAL: {
val = this._getFromLocalStorage()[key];
break;
}
case LOCATION.WINDOW: {
val = this._getFromWindow()[key];
break;
}
default:
console.error('StorageManager.load cannot load item with unsupported location');
}
if (val !== undefined || locations.length === 0) {
return val;
}
}
}
remove(key, options=this._options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.load called with unsupported location option');
}
const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
for (const location of locations) {
switch (location) {
case LOCATION.LOCAL: {
let val = this._getFromLocalStorage();
delete val[key];
return this._saveToLocalStorage(val);
}
case LOCATION.WINDOW: {
let val = this._getFromWindow();
delete val[key];
return this._saveToWindow(val);
}
default:
console.error('StorageManager.load cannot load item with unsupported location');
}
}
}
clear(options) {
if (options && options.location !== undefined && !Object.values(LOCATION).includes(options.location)) {
throw new Error('StorageManager.clear called with unsupported location option');
}
const locations = options && options.location !== undefined ? [options.location] : LOCATION_SHADOWING;
for (const location of locations) {
switch (location) {
case LOCATION.LOCAL:
return this._clearLocalStorage();
case LOCATION.WINDOW:
return this._clearWindow();
default:
console.error('StorageManager.clear cannot clear with unsupported location');
}
}
}
_getFromLocalStorage() {
let state;
try {
state = JSON.parse(window.localStorage.getItem(this.namespace));
} catch {
state = null;
}
if (state === null || !state.version || !semver.satisfies(this.version, `^${state.version}`)) {
// remove item from localStorage if it stores an invalid state
this._clearLocalStorage();
return {};
}
if ('state' in state)
return state.state;
else {
delete state.version;
return state;
}
}
_saveToLocalStorage(state) {
if (!state)
return this._clearLocalStorage();
let versionedState;
if ('version' in state || 'state' in state) {
versionedState = { version: this.version, state: state };
} else {
versionedState = { version: this.version, ...state };
}
window.localStorage.setItem(this.namespace, JSON.stringify(versionedState));
}
_clearLocalStorage() {
window.localStorage.removeItem(this.namespace);
}
_getFromWindow() {
if (!this._global || !this._global.App)
return {};
if (!this._global.App.Storage)
this._global.App.Storage = {};
return this._global.App.Storage[this.namespace] || {};
}
_saveToWindow(value) {
if (!this._global || !this._global.App) {
throw new Error('StorageManager._saveToWindow called when window.App is not available');
}
if (!value)
return this._clearWindow();
if (!this._global.App.Storage)
this._global.App.Storage = {};
this._global.App.Storage[this.namespace] = value;
}
_clearWindow() {
if (!this._global || !this._global.App) {
throw new Error('StorageManager._saveToWindow called when window.App is not available');
}
if (this._global.App.Storage) {
delete this._global.App.Storage[this.namespace];
}
}
}

4
frontend/src/polyfill.js Normal file
View File

@ -0,0 +1,4 @@
import 'whatwg-fetch';
import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
window.ResizeObserver = window.ResizeObserver || Polyfill;

View File

@ -1,3 +1,7 @@
import moment from 'moment';
import 'moment/locale/en-ie.js';
import 'moment/locale/de.js';
/**
* I18n
*
@ -13,10 +17,15 @@
export class I18n {
translations = {};
_translations = {};
_datetimeLocale = undefined;
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) {
@ -24,9 +33,27 @@ export class I18n {
}
get(id) {
if (!this.translations[id]) {
if (!this._translations[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;
}
}

View File

@ -9,7 +9,7 @@ describe('I18n', () => {
// helper function
function expectTranslation(id, value) {
expect(i18n.translations[id]).toMatch(value);
expect(i18n.get(id)).toMatch(value);
}
it('should create', () => {
@ -38,7 +38,7 @@ describe('I18n', () => {
describe('get()', () => {
it('should return stored translations', () => {
i18n.translations.id1 = 'something';
i18n.add('id1', 'something');
expect(i18n.get('id1')).toMatch('something');
});

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './alerts.scss';
import './alerts.sass';
const ALERTS_INITIALIZED_CLASS = 'alerts--initialized';
const ALERTS_ELEVATED_CLASS = 'alerts--elevated';

View File

@ -0,0 +1,186 @@
@use "../../common" as *
.alerts
position: fixed
bottom: 0
right: 5%
z-index: 20
text-align: right
display: flex
flex-direction: column
.alerts__toggler
width: 40px
height: 40px
position: absolute
top: 400px
left: 50%
transform: translateX(-50%)
cursor: pointer
&::before
@extend .fas
content: fa-content($fa-var-chevron-up)
position: absolute
left: 50%
top: 0
height: 30px
display: flex
align-items: center
justify-content: center
width: 30px
color: var(--color-grey)
font-size: 30px
transform: translateX(-50%)
&:hover::before
color: var(--color-grey-medium)
.alerts--elevated
z-index: 1000
.alerts__toggler--visible
top: -40px
opacity: 1
transition: top 0.5s cubic-bezier(0.73, 1.25, 0.61, 1), opacity 0.5s cubic-bezier(0.73, 1.25, 0.61, 1)
@media (max-width: 425px)
.alerts
left: 5%
.alert
position: relative
display: block
background-color: var(--color-lightblack)
font-size: 1rem
color: var(--color-lightwhite)
z-index: 0
padding: 0 50px
padding-right: 60px
animation: slide-in-alert .2s ease-out forwards
margin-bottom: 10px
transition: margin-bottom .2s ease-out
.alert a
color: var(--color-lightwhite)
&:hover
color: var(--color-grey)
@keyframes slide-in-alert
from
transform: translateY(120%)
to
transform: translateY(0)
@keyframes slide-out-alert
from
transform: translateY(0)
max-height: 200px
to
transform: translateY(250%)
opacity: 0
max-height: 0
overflow: hidden
@media (min-width: 425px)
.alert
max-width: 400px
.alert--invisible
animation: slide-out-alert .2s ease-out forwards
margin-bottom: 0
.alert__content
padding: 8px 0
min-height: 40px
position: relative
display: flex
font-weight: 600
align-items: center
text-align: left
.alert__icon
text-align: right
position: absolute
left: 0px
top: 0
width: 50px
height: 100%
z-index: 40
&::before
position: absolute
font-size: 24px
top: 50%
left: 50%
display: flex
align-items: center
justify-content: center
transform: translate(-50%, -50%)
border-radius: 50%
width: 30px
height: 30px
.alert__closer
cursor: pointer
text-align: right
position: absolute
right: 0px
top: 0
width: 60px
height: 100%
transition: all .3s ease
z-index: 40
&:hover
transform: scale(1.05, 1.05)
&::before
box-shadow: 0 0 4px white
background-color: rgba(255, 255, 255, 0.1)
color: white
&::before
@extend .fas
content: fa-content($fa-var-times)
position: absolute
top: 50%
left: 50%
display: flex
align-items: center
justify-content: center
transform: translate(-50%, -50%)
border-radius: 50%
width: 30px
height: 30px
transition: all .15s ease
@media (max-width: 768px)
.alert__closer
width: 40px
.alert-success
background-color: var(--color-success)
// .alert__icon::before {
// --alert-icon-default: '\f058';
// }
.alert-warning
background-color: var(--color-warning)
// .alert__icon::before {
// --alert-icon-default: '\f06a';
// }
.alert-error
background-color: var(--color-error)
// .alert__icon::before {
// --alert-icon-default: '\f071';
// }

View File

@ -1,221 +0,0 @@
.alerts {
position: fixed;
bottom: 0;
right: 5%;
z-index: 20;
text-align: right;
display: flex;
flex-direction: column;
}
.alerts__toggler {
width: 40px;
height: 40px;
position: absolute;
top: 400px;
left: 50%;
transform: translateX(-50%);
cursor: pointer;
&::before {
content: '\f077';
position: absolute;
font-family: 'Font Awesome 5 Free';
left: 50%;
top: 0;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
color: var(--color-grey);
font-size: 30px;
transform: translateX(-50%);
}
&:hover::before {
color: var(--color-grey-medium);
}
}
.alerts--elevated {
z-index: 1000;
}
.alerts__toggler--visible {
top: -40px;
opacity: 1;
transition: top .5s cubic-bezier(0.73, 1.25, 0.61, 1),
opacity .5s cubic-bezier(0.73, 1.25, 0.61, 1);
}
@media (max-width: 425px) {
.alerts {
left: 5%;
}
}
.alert {
position: relative;
display: block;
background-color: var(--color-lightblack);
font-size: 1rem;
color: var(--color-lightwhite);
z-index: 0;
padding: 0 50px;
padding-right: 60px;
animation: slide-in-alert .2s ease-out forwards;
margin-bottom: 10px;
transition: margin-bottom .2s ease-out;
}
.alert a {
color: var(--color-lightwhite);
&:hover {
color: var(--color-grey);
}
}
@keyframes slide-in-alert {
from {
transform: translateY(120%);
}
to {
transform: translateY(0);
}
}
@keyframes slide-out-alert {
from {
transform: translateY(0);
max-height: 200px;
}
to {
transform: translateY(250%);
opacity: 0;
max-height: 0;
overflow: hidden;
}
}
@media (min-width: 425px) {
.alert {
max-width: 400px;
}
}
.alert--invisible {
animation: slide-out-alert .2s ease-out forwards;
margin-bottom: 0;
}
.alert__content {
padding: 8px 0;
min-height: 40px;
position: relative;
display: flex;
font-weight: 600;
align-items: center;
text-align: left;
}
.alert__icon {
text-align: right;
position: absolute;
left: 0px;
top: 0;
width: 50px;
height: 100%;
z-index: 40;
&::before {
/* content: var(--alert-icon, var(--alert-icon-default, '\f05a')); */
position: absolute;
/* font-family: 'Font Awesome 5 Free'; */
font-size: 24px;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
border-radius: 50%;
width: 30px;
height: 30px;
}
}
.alert__closer {
cursor: pointer;
text-align: right;
position: absolute;
right: 0px;
top: 0;
width: 60px;
height: 100%;
transition: all .3s ease;
z-index: 40;
&:hover {
transform: scale(1.05, 1.05);
&::before {
box-shadow: 0 0 4px white;
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
}
&::before {
content: '\f00d';
position: absolute;
font-family: 'Font Awesome 5 Free';
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
border-radius: 50%;
width: 30px;
height: 30px;
transition: all .15s ease;
}
}
@media (max-width: 768px) {
.alert__closer {
width: 40px;
}
}
.alert-success {
background-color: var(--color-success);
/* .alert__icon::before {
* --alert-icon-default: '\f058';
* }
*/
}
.alert-warning {
background-color: var(--color-warning);
/* .alert__icon::before {
* --alert-icon-default: '\f06a';
* }
*/
}
.alert-error {
background-color: var(--color-error);
/* .alert__icon::before {
* --alert-icon-default: '\f071';
* }
*/
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './asidenav.scss';
import './asidenav.sass';
const FAVORITES_BTN_CLASS = 'navbar__list-item--favorite';
const FAVORITES_BTN_ACTIVE_CLASS = 'navbar__list-item--active';

View File

@ -0,0 +1,330 @@
.main__aside
position: fixed
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
z-index: 1
top: 0
left: 0
width: var(--asidenav-width-lg, 20%)
height: 100%
flex: 0 0 0
flex-basis: var(--asidenav-width-lg, 20%)
transition: all .2s ease-out
&::before
position: absolute
z-index: -1
left: 0
top: 0
width: 100%
height: 100%
background-color: var(--color-dark)
opacity: 0.05
&::after
content: ''
position: absolute
z-index: -2
left: 0
top: 0
width: 100%
height: 100%
background-color: var(--color-grey-light)
@media (max-width: 425px)
.main__aside
position: fixed
top: var(--header-height-collapsed)
left: 0
right: 0
bottom: 0
height: 100% !important
width: 100%
z-index: 5
overflow: hidden
transform: translateX(-110%)
transition: transform .2s ease-out
&.main__aside--expanded
transform: translateX(0%)
.asidenav__box-subtitle
display: inherit
.asidenav__box-title
font-size: 18px
padding-left: 10px
.asidenav__box-subtitle
display: none
@media (min-width: 1200px)
.main__aside
width: var(--asidenav-width-xl, 250px)
.asidenav
color: var(--color-font)
min-height: calc(100% - var(--header-height))
height: 400px
overflow-y: auto
overflow-x: hidden
&::-webkit-scrollbar
width: 0
.asidenav__box
transition: opacity .2s ease
+ .asidenav__box
margin-top: 10px
.asidenav__box-title
padding: 7px 13px
margin-top: 30px
background-color: transparent
transition: all .2s ease
padding: 10px 13px
margin: 0
border-bottom: 1px solid var(--color-grey)
.asidenav-term-identifier--long
display: inherit
.asidenav-term-identifier--short
display: none
.asidenav__box-subtitle
color: var(--color-fontsec)
font-size: 0.9rem
font-weight: 600
padding: 0 13px
margin: 3px 0
// LOGO
.asidenav__logo
height: var(--header-height)
display: flex
align-items: center
@media (max-width: 768px)
.asidenav__logo
display: none
.asidenav__logo-link
flex: 1
top: 10px
left: 20px
height: 80px
padding: 0 20px
display: flex
flex-basis: var(--asidenav-width-xl, 250px)
font-size: 16px
align-items: center
color: var(--color-dark)
transform-origin: left
&:hover
color: var(--color-primary)
.asidenav__logo-lmu
width: 80px
height: 100%
.asidenav__logo-uni2work
display: flex
align-items: flex-end
min-width: 70px
margin-left: 12px
text-transform: uppercase
width: 100%
height: 100%
padding: 2px 4px
border: 1px solid currentColor
letter-spacing: 2px
background-color: white
transition: background-color .3s ease
@media (max-width: 1199px)
.asidenav__logo-link
flex-basis: var(--asidenav-width-lg, 20%)
font-size: 16px
.asidenav__logo-lmu
display: none
.asidenav__logo-uni2work
margin-left: 0
// SEAL
.asidenav__sigillum
position: absolute
bottom: -40px
right: 25px
opacity: 0.1
> img
width: 350px
@media (max-width: 768px)
.asidenav__sigillum
right: auto
left: 50%
transform: translateX(-50%)
// LIST-ITEM
.asidenav__list-item
color: var(--color-font)
display: flex
flex-direction: column
justify-content: flex-start
align-items: center
&:not(.asidenav__list-item--active):hover
background-color: var(--color-lightwhite)
> .asidenav__link-wrapper
color: var(--color-font)
&:hover
.asidenav__link-shorthand
// transform: scale(1.05, 1.0);
// transform-origin: right;
text-shadow: none
.asidenav__nested-list-wrapper
display: block
// small list-item-padding for medium to large screens
@media (min-width: 769px)
.asidenav__list-item
padding-left: 10px
.asidenav__list-item--active
background-color: var(--color-lightwhite)
.asidenav__link-wrapper
color: var(--color-link)
.asidenav__link-shorthand
transform: scale(1.05, 1)
transform-origin: right
text-shadow: none
.asidenav__link-wrapper
position: relative
display: flex
flex: 1
align-items: center
padding: 8px 3px
justify-content: flex-start
color: var(--color-font)
width: 100%
z-index: 1
.asidenav__link-shorthand
display: none
.asidenav__link-label
line-height: 1
// hover sub-menus
.asidenav__nested-list-wrapper
position: absolute
z-index: 10
display: none
color: var(--color-font)
background-color: var(--color-grey-light)
box-shadow: 1px 1px 1px 0px var(--color-grey)
.asidenav__nested-list
min-width: 200px
@media (max-width: 425px)
.asidenav__list-item
padding-left: 10px
.asidenav__nested-list
display: none
.asidenav__nested-list-item
position: relative
&:hover
background-color: var(--color-lightwhite)
.asidenav__link-wrapper
padding-left: 13px
padding-right: 13px
transition: all .2s ease
color: var(--color-font)
// TABLET
@media (min-width: 426px) and (max-width: 768px)
.main__aside
width: var(--asidenav-width-md, 50px)
flex-basis: var(--asidenav-width-md, 50px)
overflow: hidden
min-height: calc(100% - var(--header-height-collapsed))
top: var(--header-height-collapsed)
.asidenav__box-title
width: var(--asidenav-width-md, 50px)
font-size: 18px
text-align: center
padding: 10px 1px
word-break: break-all
background-color: var(--color-dark)
color: var(--color-lightwhite)
&:hover
background-color: var(--color-darker)
&::before
display: none
.asidenav-term-identifier--long
display: none
.asidenav-term-identifier--short
display: inherit
.asidenav__box-subtitle
padding: 0 3px
font-size: 0.85rem
.asidenav__link-shorthand
display: flex
position: static
height: 50px
width: var(--asidenav-width-md, 50px)
text-align: center
opacity: 1
font-size: 15px
line-height: 1em
margin-right: 13px
flex-shrink: 0
padding: 1px
outline: 1px solid white
word-break: break-all
align-items: center
justify-content: center
.asidenav__list-item
padding-left: 0
+ .asidenav__list-item
margin: 0
.asidenav__link-wrapper
color: var(--color-font)
padding: 0
.asidenav__nested-list,
.asidenav__link-label
display: none
.asidenav__list-item--active
.asidenav__link-wrapper
background-color: var(--color-lightwhite)

View File

@ -1,384 +0,0 @@
.main__aside {
position: fixed;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
z-index: 1;
top: 0;
left: 0;
width: var(--asidenav-width-lg, 20%);
height: 100%;
flex: 0 0 0;
flex-basis: var(--asidenav-width-lg, 20%);
transition: all .2s ease-out;
&::before {
position: absolute;
z-index: -1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--color-dark);
opacity: 0.05;
}
&::after {
content: '';
position: absolute;
z-index: -2;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--color-grey-light);
}
}
@media (max-width: 425px) {
.main__aside {
position: fixed;
top: var(--header-height-collapsed);
left: 0;
right: 0;
bottom: 0;
height: 100% !important;
width: 100%;
z-index: 5;
overflow: hidden;
transform: translateX(-110%);
transition: transform .2s ease-out;
&.main__aside--expanded {
transform: translateX(0%);
}
.asidenav__box-title {
font-size: 18px;
padding-left: 10px;
}
.asidenav__box-subtitle {
display: none;
}
}
}
@media (min-width: 1200px) {
.main__aside {
width: var(--asidenav-width-xl, 250px)
}
}
.asidenav {
color: var(--color-font);
min-height: calc(100% - var(--header-height));
height: 400px;
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0;
}
}
.asidenav__box {
transition: opacity .2s ease;
+ .asidenav__box {
margin-top: 10px;
}
}
.asidenav__box-title {
padding: 7px 13px;
margin-top: 30px;
background-color: transparent;
transition: all .2s ease;
padding: 10px 13px;
margin: 0;
border-bottom: 1px solid var(--color-grey);
}
.asidenav__box-subtitle {
color: var(--color-fontsec);
font-size: 0.9rem;
font-weight: 600;
padding: 0 13px;
margin: 3px 0;
}
/* LOGO */
.asidenav__logo {
height: var(--header-height);
display: flex;
align-items: center;
}
@media (max-width: 768px) {
.asidenav__logo {
display: none;
}
}
.asidenav__logo-link {
flex: 1;
top: 10px;
left: 20px;
height: 80px;
padding: 0 20px;
display: flex;
flex-basis: var(--asidenav-width-xl, 250px);
font-size: 16px;
align-items: center;
color: var(--color-dark);
transform-origin: left;
&:hover {
color: var(--color-primary);
}
}
.asidenav__logo-lmu {
width: 80px;
height: 100%;
}
.asidenav__logo-uni2work {
display: flex;
align-items: flex-end;
min-width: 70px;
margin-left: 12px;
text-transform: uppercase;
width: 100%;
height: 100%;
padding: 2px 4px;
border: 1px solid currentColor;
letter-spacing: 2px;
background-color: white;
transition: background-color .3s ease;
}
@media (max-width: 1199px) {
.asidenav__logo-link {
flex-basis: var(--asidenav-width-lg, 20%);
font-size: 16px;
}
.asidenav__logo-lmu {
display: none;
}
.asidenav__logo-uni2work {
margin-left: 0;
}
}
/* SEAL */
.asidenav__sigillum {
position: absolute;
bottom: -40px;
right: 25px;
opacity: 0.1;
> img {
width: 350px;
}
}
@media (max-width: 768px) {
.asidenav__sigillum {
right: auto;
left: 50%;
transform: translateX(-50%);
}
}
/* LIST-ITEM */
.asidenav__list-item {
color: var(--color-font);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
&:not(.asidenav__list-item--active):hover {
background-color: var(--color-lightwhite);
> .asidenav__link-wrapper {
color: var(--color-font);
}
}
&:hover {
.asidenav__link-shorthand {
transform: scale(1.05, 1.0);
transform-origin: right;
text-shadow: none;
}
.asidenav__nested-list-wrapper {
display: block;
}
}
}
/* small list-item-padding for medium to large screens */
@media (min-width: 769px) {
.asidenav__list-item {
padding-left: 10px;
}
}
.asidenav__list-item--active {
background-color: var(--color-lightwhite);
.asidenav__link-wrapper {
color: var(--color-link);
}
.asidenav__link-shorthand {
transform: scale(1.05, 1.0);
transform-origin: right;
text-shadow: none;
}
}
.asidenav__link-wrapper {
position: relative;
display: flex;
flex: 1;
align-items: center;
padding: 8px 3px;
justify-content: flex-start;
color: var(--color-font);
width: 100%;
z-index: 1;
}
.asidenav__link-shorthand {
display: none;
}
.asidenav__link-label {
line-height: 1;
}
/* hover sub-menus */
.asidenav__nested-list-wrapper {
position: absolute;
z-index: 10;
display: none;
color: var(--color-font);
background-color: var(--color-grey-light);
box-shadow: 1px 1px 1px 0px var(--color-grey);
}
.asidenav__nested-list {
min-width: 200px;
}
@media (max-width: 425px) {
.asidenav__nested-list {
display: none;
}
}
.asidenav__nested-list-item {
position: relative;
&:hover {
background-color: var(--color-lightwhite);
}
.asidenav__link-wrapper {
padding-left: 13px;
padding-right: 13px;
transition: all .2s ease;
color: var(--color-font);
}
}
/* TABLET */
@media (min-width: 426px) and (max-width: 768px) {
.main__aside {
width: var(--asidenav-width-md, 50px);
flex-basis: var(--asidenav-width-md, 50px);
overflow: hidden;
min-height: calc(100% - var(--header-height-collapsed));
top: var(--header-height-collapsed);
.asidenav__box-title {
width: var(--asidenav-width-md, 50px);
font-size: 18px;
text-align: center;
padding: 10px 1px;
word-break: break-all;
background-color: var(--color-dark);
color: var(--color-lightwhite);
&:hover {
background-color: var(--color-darker);
}
&::before {
display: none;
}
}
.asidenav__box-subtitle {
display: none;
}
.asidenav__link-shorthand {
display: flex;
position: static;
height: 50px;
width: var(--asidenav-width-md, 50px);
text-align: center;
opacity: 1;
font-size: 15px;
line-height: 1em;
margin-right: 13px;
flex-shrink: 0;
padding: 1px;
outline: 1px solid white;
word-break: break-all;
align-items: center;
justify-content: center;
}
.asidenav__list-item {
padding-left: 0;
+ .asidenav__list-item {
margin: 0;
}
}
.asidenav__link-wrapper {
color: var(--color-font);
padding: 0;
}
.asidenav__nested-list,
.asidenav__link-label {
display: none;
}
.asidenav__list-item--active {
.asidenav__link-wrapper {
background-color: var(--color-lightwhite);
}
}
}
}

View File

@ -1,6 +1,6 @@
import { Utility } from '../../core/utility';
import { Datepicker } from '../form/datepicker';
import './async-form.scss';
import './async-form.sass';
const ASYNC_FORM_INITIALIZED_CLASS = 'check-all--initialized';
const ASYNC_FORM_RESPONSE_CLASS = 'async-form__response';

View File

@ -0,0 +1,71 @@
.async-form__response
margin: 20px 0
position: relative
width: 100%
font-size: 18px
text-align: center
padding-top: 60px
.async-form__response::before,
.async-form__response::after
position: absolute
top: 0px
left: 50%
display: block
.async-form__response--success::before
content: ''
width: 17px
height: 28px
border: solid #069e04
border-width: 0 5px 5px 0
transform: translateX(-50%) rotate(45deg)
.async-form__response--info::before
content: ''
width: 5px
height: 30px
top: 10px
background-color: #777
transform: translateX(-50%)
.async-form__response--info::after
content: ''
width: 5px
height: 5px
background-color: #777
transform: translateX(-50%)
.async-form__response--warning::before
content: ''
width: 5px
height: 30px
background-color: rgb(255, 187, 0)
transform: translateX(-50%)
.async-form__response--warning::after
content: ''
width: 5px
height: 5px
top: 35px
background-color: rgb(255, 187, 0)
transform: translateX(-50%)
.async-form__response--error::before
content: ''
width: 5px
height: 40px
background-color: #940d0d
transform: translateX(-50%) rotate(-45deg)
.async-form__response--error::after
content: ''
width: 5px
height: 40px
background-color: #940d0d
transform: translateX(-50%) rotate(45deg)
.async-form--loading
opacity: 0.1
transition: opacity 800ms ease-out
pointer-events: none

View File

@ -1,78 +0,0 @@
.async-form__response {
margin: 20px 0;
position: relative;
width: 100%;
font-size: 18px;
text-align: center;
padding-top: 60px;
}
.async-form__response::before,
.async-form__response::after {
position: absolute;
top: 0px;
left: 50%;
display: block;
}
.async-form__response--success::before {
content: '';
width: 17px;
height: 28px;
border: solid #069e04;
border-width: 0 5px 5px 0;
transform: translateX(-50%) rotate(45deg);
}
.async-form__response--info::before {
content: '';
width: 5px;
height: 30px;
top: 10px;
background-color: #777;
transform: translateX(-50%);
}
.async-form__response--info::after {
content: '';
width: 5px;
height: 5px;
background-color: #777;
transform: translateX(-50%);
}
.async-form__response--warning::before {
content: '';
width: 5px;
height: 30px;
background-color: rgb(255, 187, 0);
transform: translateX(-50%);
}
.async-form__response--warning::after {
content: '';
width: 5px;
height: 5px;
top: 35px;
background-color: rgb(255, 187, 0);
transform: translateX(-50%);
}
.async-form__response--error::before {
content: '';
width: 5px;
height: 40px;
background-color: #940d0d;
transform: translateX(-50%) rotate(-45deg);
}
.async-form__response--error::after {
content: '';
width: 5px;
height: 40px;
background-color: #940d0d;
transform: translateX(-50%) rotate(45deg);
}
.async-form--loading {
opacity: 0.1;
transition: opacity 800ms ease-out;
pointer-events: none;
}

View File

@ -0,0 +1,4 @@
.async-table-filter--loading
opacity: 0.7
pointer-events: none
transition: opacity 400ms ease-out

View File

@ -1,5 +0,0 @@
.async-table-filter--loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 400ms ease-out;
}

View File

@ -1,9 +1,10 @@
import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import { Datepicker } from '../form/datepicker';
import { HttpClient } from '../../services/http-client/http-client';
import * as debounce from 'lodash.debounce';
import './async-table-filter.scss';
import './async-table.scss';
import './async-table-filter.sass';
import './async-table.sass';
const INPUT_DEBOUNCE = 600;
const HEADER_HEIGHT = 80;
@ -40,6 +41,8 @@ export class AsyncTable {
};
_ignoreRequest = false;
_storageManager = new StorageManager(ASYNC_TABLE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.WINDOW });
constructor(element, app) {
if (!element) {
throw new Error('Async Table utility cannot be setup without an element!');
@ -81,10 +84,10 @@ export class AsyncTable {
this._setupPageSizeSelect();
this._setupTableFilter();
this._processLocalStorage();
this._processStorage();
// clear currentTableUrl from previous requests
setLocalStorageParameter('currentTableUrl', null);
this._storageManager.remove('currentTableUrl');
// mark initialized
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
@ -100,7 +103,7 @@ export class AsyncTable {
this._ths.forEach((th) => {
th.clickHandler = (event) => {
setLocalStorageParameter('horizPos', (this._scrollTable || {}).scrollLeft);
this._storageManager.save('horizPos', (this._scrollTable || {}).scrollLeft);
this._linkClickHandler(event);
};
th.element.addEventListener('click', th.clickHandler);
@ -122,7 +125,7 @@ export class AsyncTable {
left: this._scrollTable.offsetLeft || 0,
behavior: 'smooth',
};
setLocalStorageParameter('scrollTo', scrollTo);
this._storageManager.save('scrollTo', scrollTo);
}
this._linkClickHandler(event);
};
@ -225,7 +228,7 @@ export class AsyncTable {
const prefix = findCssIdPrefix(focusedInput.id);
const focusId = focusedInput.id.replace(prefix, '');
callback = function(wrapper) {
const idPrefix = getLocalStorageParameter('cssIdPrefix');
const idPrefix = this._storageManager.load('cssIdPrefix');
const toBeFocused = wrapper.querySelector('#' + idPrefix + focusId);
if (toBeFocused) {
toBeFocused.focus();
@ -238,7 +241,7 @@ export class AsyncTable {
}
_serializeTableFilterToURL(tableFilterForm) {
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values
const formData = Datepicker.unformatAll(this._massInputForm, new FormData(tableFilterForm));
@ -254,18 +257,18 @@ export class AsyncTable {
return url;
}
_processLocalStorage() {
const scrollTo = getLocalStorageParameter('scrollTo');
_processStorage() {
const scrollTo = this._storageManager.load('scrollTo');
if (scrollTo && this._scrollTable) {
window.scrollTo(scrollTo);
}
setLocalStorageParameter('scrollTo', null);
this._storageManager.remove('scrollTo');
const horizPos = getLocalStorageParameter('horizPos');
const horizPos = this._storageManager.load('horizPos');
if (horizPos && this._scrollTable) {
this._scrollTable.scrollLeft = horizPos;
}
setLocalStorageParameter('horizPos', null);
this._storageManager.remove('horizPos');
}
_removeListeners() {
@ -300,7 +303,7 @@ export class AsyncTable {
}
_changePagesizeHandler = () => {
const url = new URL(getLocalStorageParameter('currentTableUrl') || window.location.href);
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values
const formData = Datepicker.unformatAll(this._pagesizeForm, new FormData(this._pagesizeForm));
@ -336,7 +339,7 @@ export class AsyncTable {
return false;
}
setLocalStorageParameter('currentTableUrl', url.href);
this._storageManager.save('currentTableUrl', url.href);
// reset table
this._removeListeners();
this._element.classList.remove(ASYNC_TABLE_INITIALIZED_CLASS);
@ -346,9 +349,9 @@ export class AsyncTable {
this._app.utilRegistry.setupAll(this._element);
if (callback && typeof callback === 'function') {
setLocalStorageParameter('cssIdPrefix', response.idPrefix);
this._storageManager.save('cssIdPrefix', response.idPrefix);
callback(this._element);
setLocalStorageParameter('cssIdPrefix', '');
this._storageManager.remove('cssIdPrefix');
}
}).catch((err) => console.error(err)
).finally(() => this._element.classList.remove(ASYNC_TABLE_LOADING_CLASS));
@ -365,17 +368,3 @@ function findCssIdPrefix(id) {
}
return '';
}
function setLocalStorageParameter(key, value) {
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
if (value !== null) {
currentLSState[key] = value;
} else {
delete currentLSState[key];
}
window.localStorage.setItem(ASYNC_TABLE_LOCAL_STORAGE_KEY, JSON.stringify(currentLSState));
}
function getLocalStorageParameter(key) {
const currentLSState = JSON.parse(window.localStorage.getItem(ASYNC_TABLE_LOCAL_STORAGE_KEY)) || {};
return currentLSState[key];
}

View File

@ -0,0 +1,4 @@
.async-table--loading
opacity: 0.7
pointer-events: none
transition: opacity 400ms ease-out

View File

@ -1,5 +0,0 @@
.async-table--loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 400ms ease-out;
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './course-teaser.scss';
import './course-teaser.sass';
var COURSE_TEASER_INITIALIZED_CLASS = 'course-teaser--initialized';

View File

@ -0,0 +1,246 @@
[uw-course-teaser]
--course-border-color: var(--color-grey)
--course-padding-hori: 10px
--course-padding-vert: 12px
display: grid
position: relative
grid-gap: 5px 7px
grid-template-columns: 130px 30px 1fr 60px
grid-template-areas: 'shrthnd . title chevron' 'shrthnd smstr school chevron' '. . rgstrd . ' 'tutor tutor name . ' 'duedate duedate date . ' 'dscrptn dscrptn dscrptn dscrptn'
padding: var(--course-padding-vert) var(--course-padding-hori)
transition: background-color .1s ease-out
cursor: pointer
&:hover
background-color: var(--course-bg-color)
+ [uw-course-teaser]
border-top: 1px solid var(--course-border-color)
@media (max-width: 768px)
grid-template-columns: 140px 1fr 30px
grid-template-areas: 'shrthnd title chevron' 'shrthnd title . ' 'smstr school school ' '. rgstrd rgstrd ' 'tutor name name ' 'duedate date date ' 'dscrptn dscrptn dscrptn'
@media (max-width: 426px)
grid-template-columns: 1fr
grid-template-areas: 'shrthnd' 'title' 'smstr' 'school' 'rgstrd' 'tutor' 'name' 'duedate' 'date' 'dscrptnlbl' 'dscrptn' 'chevron'
.course-teaser__not-expandable
cursor: initial
// chevron
.course-teaser__chevron
position: relative
padding: 10px
grid-area: chevron
justify-self: center
align-self: center
width: 100%
height: 100%
cursor: pointer
&::before
content: ''
position: absolute
display: block
margin-top: -7.35px
margin-left: -7.35px
// visually centered
border-width: 0 3px 3px 0
width: 8px
height: 8px
top: 50%
left: 50%
border-color: var(--color-fontsec)
border-style: solid
transform: rotate(135deg)
transform-origin: 7.25px 7.25px
// rotate about visual center
transition: all .2s ease-out
&:hover::before
transform: scale(1.4) rotate(45deg)
@media (max-width: 768px)
justify-self: end
width: auto
&::before
position: initial
@media (max-width: 426px)
&::before
transform: rotate(45deg)
margin-left: -7.35px
&:hover::before
transform: scale(1.4) rotate(45deg)
// semester
.course-teaser__semester
grid-area: smstr
justify-self: end
a
color: var(--color-fontsec)
@media (max-width: 768px)
justify-self: initial
// shorthand
.course-teaser__shorthand
position: relative
grid-area: shrthnd
font-size: 2rem
line-height: 1.25
min-height: calc(2rem * 1.25)
> a
position: absolute
height: 100%
width: 100%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
word-break: break-any
text-decoration: none !important
// sry.
font-weight: 600
color: var(--color-grey-medium)
// @media (max-width: 768px) {
position: initial;
}
// title
.course-teaser__title
grid-area: title
font-size: 1.2rem
align-self: baseline
// registration
.course-teaser__registration
grid-area: rgstrd
color: var(--color-fontsec)
font-weight: bold
// school
.course-teaser__school
grid-area: school
a
color: var(--color-fontsec)
// duedate
.course-teaser__duedate
grid-area: date
// lecturer
.course-teaser__lecturer
grid-area: name
// description
.course-teaser__description
grid-area: dscrptn
max-height: 75vh
overflow: auto
// show description only as dots (overflow text-overflow) and only show when expanded. No "hidden fiddling"
// labels
.course-teaser__lecturer-label
grid-area: tutor
.course-teaser__duedate-label
grid-area: duedate
.course-teaser__description-label
grid-area: dscrptnlbl
.course-teaser__lecturer-label,
.course-teaser__description-label,
.course-teaser__duedate-label
justify-self: end
color: var(--color-fontsec)
font-style: italic
@media (max-width: 768px)
justify-self: initial
@media (max-width: 426px)
margin-top: 7px
font-weight: bold
font-style: initial
// hidden in closed state
.course-teaser__description,
.course-teaser__description-label
display: none
// expanded courses
.course-teaser__expanded
cursor: initial
.course-teaser__chevron
&::before
transform: rotate(45deg)
&:hover::before
transform: scale(1.4) rotate(135deg)
@media (max-width: 426px)
&::before
transform: rotate(225deg)
&:hover::before
transform: scale(1.4) rotate(225deg)
.course-teaser__school-label,
.course-teaser__school,
.course-teaser__duedate-label,
.course-teaser__duedate,
.course-teaser__description
display: block
@media (max-width: 426px)
.course-teaser__description-label
display: block
// course teaser: header styling
.course-teaser-header
padding-top: 10px
padding-bottom: 20px
line-height: 1.4
max-width: 85vw
.course-header
float: left
background-color: var(--color-dark)
position: relative
font-size: 16px
color: #fff
padding-top: 10px
padding-bottom: 10px
padding-left: 10px
padding-right: 35px
margin-bottom: 10px
font-weight: bold
text-align: left
border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%
margin-right: 30px
.course-header-link
color: white
font-weight: bold
text-decoration: none
&:hover
color: inherit
.course-teaser-header:after
content: ""
display: table
clear: both

View File

@ -1,317 +0,0 @@
[uw-course-teaser] {
--course-border-color: var(--color-grey);
--course-padding-hori: 10px;
--course-padding-vert: 12px;
display: grid;
position: relative;
grid-gap: 5px 7px;
grid-template-columns: 130px 30px 1fr 60px;
grid-template-areas:
'shrthnd . title chevron'
'shrthnd smstr school chevron'
'. . rgstrd . '
'tutor tutor name . '
'duedate duedate date . '
'dscrptn dscrptn dscrptn dscrptn';
padding: var(--course-padding-vert) var(--course-padding-hori);
transition: background-color .1s ease-out;
cursor: pointer;
&:hover {
background-color: var(--course-bg-color);
}
+ [uw-course-teaser] {
border-top: 1px solid var(--course-border-color);
}
@media (max-width: 768px) {
grid-template-columns: 140px 1fr 30px;
grid-template-areas:
'shrthnd title chevron'
'shrthnd title . '
'smstr school school '
'. rgstrd rgstrd '
'tutor name name '
'duedate date date '
'dscrptn dscrptn dscrptn';
}
@media (max-width: 426px) {
grid-template-columns: 1fr;
grid-template-areas:
'shrthnd'
'title'
'smstr'
'school'
'rgstrd'
'tutor'
'name'
'duedate'
'date'
'dscrptnlbl'
'dscrptn'
'chevron';
}
}
.course-teaser__not-expandable {
cursor: initial;
}
/* chevron */
.course-teaser__chevron {
position: relative;
padding: 10px;
grid-area: chevron;
justify-self: center;
align-self: center;
width: 100%;
height: 100%;
cursor: pointer;
&::before {
content: '';
position: absolute;
display: block;
margin-top: -7.35px;
margin-left: -7.35px; /* visually centered */
border-width: 0 3px 3px 0;
width: 8px;
height: 8px;
top: 50%;
left: 50%;
border-color: var(--color-fontsec);
border-style: solid;
transform: rotate(135deg);
transform-origin: 7.25px 7.25px; /* rotate about visual center */
transition: all .2s ease-out;
}
&:hover::before {
transform: scale(1.4) rotate(45deg);
}
@media (max-width: 768px) {
justify-self: end;
width: auto;
&::before {
position: initial;
}
}
@media (max-width: 426px) {
&::before {
transform: rotate(45deg);
margin-left: -7.35px;
}
&:hover::before {
transform: scale(1.4) rotate(45deg);
}
}
}
/* semester */
.course-teaser__semester {
grid-area: smstr;
justify-self: end;
a {
color: var(--color-fontsec);
}
@media (max-width: 768px) {
justify-self: initial;
}
}
/* shorthand */
.course-teaser__shorthand {
position: relative;
grid-area: shrthnd;
font-size: 2rem;
line-height: 1.25;
min-height: calc(2rem * 1.25);
> a {
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-any;
text-decoration: none !important; /* sry. */
font-weight: 600;
color: var(--color-grey-medium);
}
/* @media (max-width: 768px) {
* position: initial;
* }
*/
}
/* title */
.course-teaser__title {
grid-area: title;
font-size: 1.2rem;
align-self: baseline;
}
/* registration */
.course-teaser__registration {
grid-area: rgstrd;
color: var(--color-fontsec);
font-weight: bold;
}
/* school */
.course-teaser__school {
grid-area: school;
a {
color: var(--color-fontsec);
}
}
/* duedate */
.course-teaser__duedate {
grid-area: date;
}
/* lecturer */
.course-teaser__lecturer {
grid-area: name;
}
/* description */
.course-teaser__description {
grid-area: dscrptn;
max-height: 75vh;
overflow: auto;
}
/* show description only as dots (overflow text-overflow) and only show when expanded. No "hidden fiddling" */
/* labels */
.course-teaser__lecturer-label {
grid-area: tutor;
}
.course-teaser__duedate-label {
grid-area: duedate;
}
.course-teaser__description-label {
grid-area: dscrptnlbl;
}
.course-teaser__lecturer-label,
.course-teaser__description-label,
.course-teaser__duedate-label {
justify-self: end;
color: var(--color-fontsec);
font-style: italic;
@media (max-width: 768px) {
justify-self: initial;
}
@media (max-width: 426px) {
margin-top: 7px;
font-weight: bold;
font-style: initial;
}
}
/* hidden in closed state */
.course-teaser__description,
.course-teaser__description-label {
display: none;
}
/* expanded courses */
.course-teaser__expanded {
cursor: initial;
.course-teaser__chevron {
&::before {
transform: rotate(45deg);
}
&:hover::before {
transform: scale(1.4) rotate(135deg);
}
@media (max-width: 426px) {
&::before {
transform: rotate(225deg);
}
&:hover::before {
transform: scale(1.4) rotate(225deg);
}
}
}
.course-teaser__school-label,
.course-teaser__school,
.course-teaser__duedate-label,
.course-teaser__duedate,
.course-teaser__description {
display: block;
}
@media (max-width: 426px) {
.course-teaser__description-label {
display: block;
}
}
}
/*
course teaser: header styling
*/
.course-teaser-header {
padding-top: 10px;
padding-bottom: 20px;
line-height: 1.4;
max-width: 85vw;
.course-header {
float: left;
background-color: var(--color-dark);
position: relative;
font-size: 16px;
color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 35px;
margin-bottom: 10px;
font-weight: bold;
text-align: left;
border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%;
margin-right: 30px;
.course-header-link {
color: white;
font-weight: bold;
text-decoration: none;
&:hover {
color: inherit;
}
}
}
}
.course-teaser-header:after {
content: "";
display: table;
clear: both;
}

View File

@ -1,4 +1,5 @@
import datetime from 'tail.datetime';
import './datepicker.css';
import { Utility } from '../../core/utility';
import moment from 'moment';
@ -33,7 +34,7 @@ const FORM_DATE_FORMAT_MOMENT = {
* @param {*} formatOut format string of the desired output date string
*/
function reformatDateString(dateStr, formatIn, formatOut) {
const parsedMomentDate = moment(dateStr, formatIn);
const parsedMomentDate = moment(dateStr, [formatIn, formatOut]);
return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr;
}
@ -48,9 +49,6 @@ const DATEPICKER_CONFIG = {
timeMinutes: 0,
timeSeconds: 0,
// german settings
// TODO: hardcoded, get from current language / settings
locale: 'de',
weekStart: 1,
dateFormat: FORM_DATE_FORMAT_DATE_DT,
timeFormat: FORM_DATE_FORMAT_TIME_DT,
@ -86,6 +84,7 @@ export class Datepicker {
datepickerInstance;
_element;
elementType;
_locale;
constructor(element) {
if (!element) {
@ -96,6 +95,8 @@ export class Datepicker {
return false;
}
this._locale = window.App.i18n.getDatetimeLocale();
// initialize datepickerCollections singleton if not already done
if (!Datepicker.datepickerCollections) {
Datepicker.datepickerCollections = new Map();
@ -123,11 +124,22 @@ export class Datepicker {
throw new Error('Datepicker utility called on unsupported element!');
}
// format any existing dates to fancy display format on pageload
this.formatElementValue(true);
// FIXME dirty hack below; fix tail.datetime instead
// initialize tail.datetime (datepicker) instance
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
// get date object from internal format before datetime does nasty things with it
var parsedMomentDate = moment(this._element.value, [ FORM_DATE_FORMAT[this.elementType], FORM_DATE_FORMAT_MOMENT[this.elementType] ], true);
if (parsedMomentDate && parsedMomentDate.isValid()) {
parsedMomentDate = parsedMomentDate.toDate();
} else {
parsedMomentDate = undefined;
}
// initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale });
// reset date to something sane
if (parsedMomentDate)
this.datepickerInstance.selectDate(parsedMomentDate);
// insert the datepicker element (dt) after the form
this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling);
@ -169,14 +181,27 @@ export class Datepicker {
// 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 });
// 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
window.addEventListener('click', event => {
if (!this.datepickerInstance.dt.contains(event.target) && window.document.contains(event.target)) {
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
window.addEventListener('focusout', event => {
const hasFocus = event.relatedTarget !== null;
const focussedIsNotTimepicker = !this.datepickerInstance.dt.contains(event.relatedTarget);
const focussedIsNotElement = event.relatedTarget !== this._element;
const focussedIsInDocument = window.document.contains(event.relatedTarget);
if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument)
this.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();
}
});
// close the datepicker on escape keydown events
// close the instance on escape keydown events
this._element.addEventListener('keydown', event => {
if (event.keyCode === KEYCODE_ESCAPE) {
this.datepickerInstance.close();

View File

@ -1,4 +1,4 @@
import './form.scss';
import './form.sass';
import { AutoSubmitButton } from './auto-submit-button';
import { AutoSubmitInput } from './auto-submit-input';
import { Datepicker } from './datepicker';

View File

@ -0,0 +1,35 @@
fieldset
border: 0
margin: 0
padding: 0
legend
display: none
@media (min-width: 769px)
.form-group__input
grid-column: 2
[uw-auto-submit-button][type='submit']
animation: fade-in 500ms ease-in-out backwards
animation-delay: 500ms
@keyframes fade-in
from
opacity: 0
.hidden
visibility: hidden !important
height: 0 !important
width: 0 !important
opacity: 0 !important
margin: 0 !important
padding: 0 !important
min-width: 0 !important
.select--pagesize
width: 5em
min-width: 75px
.label-pagesize
margin-right: 13px

View File

@ -1,45 +0,0 @@
fieldset {
border: 0;
margin: 0;
padding: 0;
legend {
display: none;
}
}
@media (min-width: 769px) {
.form-group__input {
grid-column: 2;
}
}
[uw-auto-submit-button][type='submit'] {
animation: fade-in 500ms ease-in-out backwards;
animation-delay: 500ms;
}
@keyframes fade-in {
from {
opacity: 0;
}
}
.hidden {
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
opacity: 0 !important;
margin: 0 !important;
padding: 0 !important;
min-width: 0 !important;
}
.select--pagesize {
width: 5em;
min-width: 75px;
}
.label-pagesize {
margin-right: 13px;
}

View File

@ -0,0 +1,312 @@
import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import './hide-columns.sass';
const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header';
const TABLE_UTILS_ATTR = 'table-utils';
const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`;
const TABLE_HIDER_CLASS = 'table-hider';
const TABLE_HIDER_VISIBLE_CLASS = 'table-hider--visible';
const TABLE_PILL_CLASS = 'table-pill';
const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell';
const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan';
@Utility({
selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`,
})
export class HideColumns {
_storageManager = new StorageManager('HIDE_COLUMNS', '1.0.0', { location: LOCATION.LOCAL });
_element;
_elementWrapper;
_tableUtilContainer;
_autoHide;
headerToHider = new Map();
hiderToHeader = new Map();
addHeaderHider(th, hider) {
this.headerToHider.set(th, hider);
this.hiderToHeader.set(hider, th);
}
constructor(element) {
this._autoHide = this._storageManager.load('autoHide', {}) || false;
if (!element) {
throw new Error('Hide Columns utility cannot be setup without an element!');
}
// do not provide hide-column ability in tables inside modals or async forms with response
if (element.closest('[uw-modal], .async-form__response')) {
return false;
}
this._element = element;
const hideColumnsContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}]`);
if (!hideColumnsContainer) {
throw new Error('Hide Columns utility needs to be setup on a table inside a hide columns container!');
}
this._elementWrapper = hideColumnsContainer;
// get or create table utils container
this._tableUtilContainer = hideColumnsContainer.querySelector(TABLE_UTILS_CONTAINER_SELECTOR);
if (!this._tableUtilContainer) {
this._tableUtilContainer = document.createElement('div');
this._tableUtilContainer.setAttribute(TABLE_UTILS_ATTR, '');
const tableContainer = this._element.closest(`[${HIDE_COLUMNS_CONTAINER_IDENT}] > *`);
hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer);
}
this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th));
}
setupHideButton(th) {
const preHidden = this.isHiddenColumn(th);
const hider = document.createElement('span');
const hiderIcon = document.createElement('i');
hiderIcon.classList.add('fas', 'fa-fw');
hider.appendChild(hiderIcon);
const hiderContent = document.createElement('span');
hiderContent.classList.add('table-hider__label');
hiderContent.innerHTML = th.innerText;
hider.appendChild(hiderContent);
this.addHeaderHider(th, hider);
th.addEventListener('mouseover', () => {
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
});
th.addEventListener('mouseout', () => {
if (hider.classList.contains(TABLE_HIDER_CLASS)) {
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
}
});
hider.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.switchColumnDisplay(th, hider);
// this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider));
});
hider.addEventListener('mouseover', () => {
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
const currentlyHidden = this.isHiddenColumn(th);
this.updateHiderIcon(hider, !currentlyHidden);
});
hider.addEventListener('mouseout', () => {
if (hider.classList.contains(TABLE_HIDER_CLASS)) {
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
}
const currentlyHidden = this.isHiddenColumn(th);
this.updateHiderIcon(hider, currentlyHidden);
});
new ResizeObserver(() => { this.repositionHider(hider); }).observe(th);
// reposition hider on each window resize event
// window.addEventListener('resize', () => this.repositionHider(hider));
this.updateColumnDisplay(this.colIndex(th), preHidden);
this.updateHider(hider, preHidden);
if (preHidden) {
this._tableUtilContainer.appendChild(hider);
} else {
this.hideHiderBehindHeader(hider);
}
}
switchColumnDisplay(th, hider) {
const hidden = !this.isHiddenColumn(th);
const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1;
const colspan = Math.max(1, th.colSpan) || 1;
const columnIndex = this.colIndex(th);
for (var i = 0; i < Math.max(colspan, originalColspan); i++) {
this.updateColumnDisplay(columnIndex + i, hidden);
}
this.updateHider(hider, hidden);
// persist new hidden setting for column
if ((hidden && this.isEmptyColumn(columnIndex) && this._autoHide) || (!hidden && (!this.isEmptyColumn(columnIndex) || !this._autoHide))) {
this._storageManager.remove(this.getStorageKey(th));
} else {
this._storageManager.save(this.getStorageKey(th), hidden);
}
}
updateColumnDisplay(columnIndex, hidden) {
this._element.getElementsByTagName('tr').forEach(row => {
const cell = this.getCol(row, columnIndex);
if (cell) {
const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
const colspan = Math.max(1, cell.colSpan) || 1;
if (hidden) {
if (colspan > 1) {
if (!originalColspan) {
cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan);
}
cell.colSpan--;
} else {
cell.classList.add(CELL_HIDDEN_CLASS);
}
} else {
if (cell.classList.contains(CELL_HIDDEN_CLASS)) {
cell.classList.remove(CELL_HIDDEN_CLASS);
} else if (originalColspan && colspan < originalColspan) {
cell.colSpan++;
}
}
}
});
}
updateHider(hider, hidden) {
if (hidden) {
hider.classList.remove(TABLE_HIDER_CLASS);
hider.classList.add(TABLE_PILL_CLASS);
this._tableUtilContainer.appendChild(hider);
} else {
hider.classList.remove(TABLE_PILL_CLASS);
hider.classList.add(TABLE_HIDER_CLASS);
this.hideHiderBehindHeader(hider);
}
this.updateHiderIcon(hider, hidden);
}
updateHiderIcon(hider, hidden) {
hider.getElementsByClassName('fas').forEach(hiderIcon => {
hiderIcon.classList.remove(hidden ? 'fa-eye' : 'fa-eye-slash');
hiderIcon.classList.add(hidden ? 'fa-eye-slash' : 'fa-eye');
});
}
hideHiderBehindHeader(hider) {
if (!this.hiderToHeader.get(hider).contains(hider)) {
this.hiderToHeader.get(hider).appendChild(hider);
}
this.repositionHider(hider);
// remove visible class if necessary
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
}
repositionHider(hider) {
const thR = this.hiderToHeader.get(hider).getBoundingClientRect(),
hR = hider.getBoundingClientRect();
hider.style.left = (thR.width/2 - hR.width/2) + 'px';
hider.style.top = thR.height + 'px';
}
getStorageKey(th) {
// get handler name
const handlerIdent = document.querySelector('[uw-handler]').getAttribute('uw-handler');
// get hide-columns container ident (if not present, use table index in document as fallback)
let tIdent = this._elementWrapper.getAttribute(HIDE_COLUMNS_CONTAINER_IDENT);
if (!tIdent) {
const tablesInDocument = document.getElementsByTagName('TABLE');
for (let i = 0; i < tablesInDocument.length; i++) {
if (tablesInDocument[i] === this._element) {
tIdent = i;
break;
}
}
}
// check for unique table header ident from backend (if not present, use cell index as fallback)
let thIdent = th.getAttribute(TABLE_HEADER_IDENT);
if (!thIdent) {
thIdent = this.colIndex(th);
}
return `${handlerIdent}__${tIdent}__${thIdent}`;
}
isEmptyColumn(columnIndex) {
for (let row of this._element.getElementsByTagName('tr')) {
const cell = this.getCol(row, columnIndex);
if (cell.matches('th'))
continue;
if (cell.querySelector('.table__td-content')) {
for (let child of cell.children) {
if (!isEmptyElement(child))
return false;
}
return true;
} else {
return isEmptyElement(cell);
}
}
}
isHiddenColumn(th) {
const hidden = this._storageManager.load(this.getStorageKey(th)),
emptyColumn = this.isEmptyColumn(this.colIndex(th));
return hidden === true || hidden === undefined && emptyColumn && this._autoHide;
}
colSpan(cell) {
if (!cell)
return 1;
const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
const colspan = Math.max(1, cell.colSpan) || 1;
return originalColspan ? Math.max(colspan, originalColspan) : colspan;
}
colIndex(cell) {
if (!cell)
return 0;
const rowParent = cell.closest('tr');
if (!rowParent)
return 0;
var i = 0;
for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
i += this.colSpan(sibling);
}
return i;
}
getCol(row, columnIndex) {
var c = 0;
for (const cell of row.cells) {
c += cell ? this.colSpan(cell) : 1;
if (columnIndex < c)
return cell;
}
}
}
function isEmptyElement(element) {
for (let child of element.childNodes) {
if (child.nodeName !== '#comment')
return false;
}
return true;
}

View File

@ -0,0 +1,61 @@
.table-hider
background-color: #fff
color: var(--color-link)
padding: 10px
cursor: pointer
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.6)
position: absolute
overflow: hidden
transition: transform .2s ease
transform: scaleY(0)
transform-origin: top
&:hover
background-color: var(--color-grey-light)
.table-hider__label
display: none
&.table-hider--visible
transform: scaleY(0.4)
transition: none
// TODO find better way to prevent transition on icons
.fas
transform: scaleY(2.5)
&:hover
transform: scaleY(1)
.fas
transform: scaleY(1)
[table-utils]
max-width: 85vw
margin-bottom: 10px
min-height: 0
line-height: 1.4
.table-pill
background-color: var(--color-dark)
float: left
color: #fff
padding: 10px
border-radius: 20px / 50%
margin-right: 20px
margin-bottom: 10px
cursor: pointer
.table-hider__label
font-size: 16px
font-weight: bold
margin-left: 5px
&:after
content: ""
display: block
clear: both
.hide-columns--hidden-cell
display: none

View File

@ -1,11 +1,11 @@
import { Utility } from '../../core/utility';
import './checkbox.scss';
import './checkbox.sass';
var CHECKBOX_CLASS = 'checkbox';
var CHECKBOX_INITIALIZED_CLASS = 'checkbox--initialized';
@Utility({
selector: 'input[type="checkbox"]',
selector: 'input[type="checkbox"]:not([uw-no-checkbox])',
})
export class Checkbox {

View File

@ -0,0 +1,71 @@
// CUSTOM CHECKBOXES
// Completely replaces legacy checkbox
.checkbox [type='checkbox'], #lang-checkbox
position: fixed
top: -1px
left: -1px
width: 1px
height: 1px
overflow: hidden
display: none
.checkbox
position: relative
display: inline-block
label
display: block
height: 20px
width: 20px
background-color: #f3f3f3
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
border: 2px solid var(--color-primary)
border-radius: 4px
color: white
cursor: pointer
label::before,
label::after
position: absolute
display: block
top: 12px
left: 8px
height: 2px
width: 8px
background-color: var(--color-font)
\:checked + label
background-color: var(--color-primary)
[type='checkbox']:focus + label
border-color: #3273dc
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
outline: 0
\:checked + label::before,
:checked + label::after
content: ''
\:checked + label::before
background-color: white
transform: rotate(45deg)
left: 2px
top: 11px
\:checked + label::after
background-color: white
transform: rotate(-45deg)
top: 9px
width: 12px
left: 7px
[disabled] + label
pointer-events: none
border: none
opacity: 0.6
filter: grayscale(1)
// special treatment for checkboxes in table headers
th .checkbox
margin-right: 7px
vertical-align: bottom

View File

@ -1,82 +0,0 @@
/* CUSTOM CHECKBOXES */
/* Completely replaces legacy checkbox */
.checkbox {
position: relative;
display: inline-block;
[type='checkbox'] {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
display: block;
height: 20px;
width: 20px;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05);
border: 2px solid var(--color-primary);
border-radius: 4px;
color: white;
cursor: pointer;
}
label::before,
label::after {
position: absolute;
display: block;
top: 12px;
left: 8px;
height: 2px;
width: 8px;
background-color: var(--color-font);
}
:checked + label {
background-color: var(--color-primary);
}
[type='checkbox']:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
:checked + label::before,
:checked + label::after {
content: '';
}
:checked + label::before {
background-color: white;
transform: rotate(45deg);
left: 2px;
top: 11px;
}
:checked + label::after {
background-color: white;
transform: rotate(-45deg);
top: 9px;
width: 12px;
left: 7px;
}
[disabled] + label {
pointer-events: none;
border: none;
opacity: 0.6;
filter: grayscale(1);
}
}
/* special treatment for checkboxes in table headers */
th .checkbox {
margin-right: 7px;
vertical-align: bottom;
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './file-input.scss';
import './file-input.sass';
const FILE_INPUT_CLASS = 'file-input';
const FILE_INPUT_INITIALIZED_CLASS = 'file-input--initialized';

View File

@ -0,0 +1,2 @@
.file-input__list:empty
display: none

View File

@ -1,3 +0,0 @@
.file-input__list:empty {
display: none;
}

View File

@ -1,8 +1,8 @@
import { Checkbox } from './checkbox';
import { FileInput } from './file-input';
import './inputs.scss';
import './radio.scss';
import './inputs.sass';
import './radio.sass';
export const InputUtils = [
Checkbox,

View File

@ -0,0 +1,211 @@
// GENERAL STYLES FOR FORMS
// FORM GROUPS
.form-section-title
color: var(--color-fontsec)
margin: 0
+ .form-group
margin-top: 11px
.form-group
position: relative
display: flex
display: grid
grid-template-columns: 1fr 3fr
grid-gap: 5px
justify-content: flex-start
align-items: flex-start
+ .form-group, + .form-section-legend, + .form-section-notification
margin-top: 11px
+ .form-section-title
margin-top: 40px
.form-section-legend
color: var(--color-fontsec)
margin: 7px 0
.form-group__hint, .form-section-title__hint
color: var(--color-fontsec)
font-size: 0.9rem
font-weight: 600
.form-section-title__hint
margin-top: 7px
+ .form-group
margin-top: 11px
.form-group-label
font-weight: 600
padding-top: 6px
.form-group-label__hint
margin-top: 7px
color: var(--color-fontsec)
font-size: 0.9rem
.form-group--required .form-group-label__caption::after, .form-group__required-marker::before
content: ' *'
color: var(--color-error)
font-weight: 600
.form-group--submit .form-group__input
grid-column: 2
@media (max-width: 768px)
.form-group--submit .form-group__input
grid-column: 1
.form-group--has-error
background-color: rgba(255, 0, 0, 0.1)
input, textarea
border-color: var(--color-error) !important
.form-error
display: block
.form-error
display: none
@media (max-width: 768px)
.form-group
grid-template-columns: 1fr
align-items: baseline
margin-top: 17px
flex-direction: column
// TEXT INPUTS
input[type='text'],
input[type='search'],
input[type='password'],
input[type='url'],
input[type='number'],
input[type='email'],
input[type*='date'],
input[type*='time'],
select
// from bulma.css
color: #363636
border-color: #dbdbdb
background-color: #f3f3f3
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
width: 100%
max-width: 600px
align-items: center
border: 1px solid transparent
border-radius: 4px
font-size: 1rem
font-family: var(--font-base)
line-height: 1.5
padding: 4px 13px
input[type='number']
width: 100px
input[type*='date'],
input[type*='time'],
.flatpickr-input[type='text']
width: 50%
width: 250px
// BUTTON STYLE SEE default-layout.lucius
// TEXTAREAS
textarea
width: 100%
height: 170px
max-width: 600px
line-height: 1.5
color: #363636
background-color: #f3f3f3
padding: 4px 13px
font-size: 1rem
font-family: var(--font-base)
appearance: none
border: 1px solid #dbdbdb
border-radius: 2px
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
vertical-align: top
// SHARED STATE RELATED STYLES
input,
select,
textarea
&:focus
border-color: #3273dc
box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25)
outline: 0
&[disabled]
background-color: #f3f3f3
color: #7a7a7a
box-shadow: none
border-color: #dbdbdb
&[readonly]
background-color: #f5f5f5
border-color: #dbdbdb
// OPTIONS
select[size="1"], select:not([size])
appearance: menulist
select,
option
font-size: 1rem
line-height: 1.5
padding: 4px 13px
border: 1px solid #dbdbdb
border-radius: 2px
outline: 0
color: #363636
min-width: 250px
width: auto
background-color: #f3f3f3
box-shadow: inset 0 1px 2px 1px rgba(50, 50, 50, 0.05)
@media (max-width: 425px)
select, option
width: 100%
// FILE INPUT
.file-input
display: none
.file-input__label
cursor: pointer
display: inline-block
background-color: var(--color-primary)
color: white
padding: 10px 17px
border-radius: 3px
.file-input__info
font-size: .9rem
font-style: italic
margin: 10px 0
color: var(--color-fontsec)
.file-input__list
margin-left: 40px
margin-top: 10px
font-weight: 600
// PREVIOUSLY UPLOADED FILES
.file-uploads-label
margin-bottom: 10px
.file-container
display: flex
align-items: center
margin-bottom: 10px
.checkbox
margin-left: 12px

View File

@ -1,254 +0,0 @@
/* GENERAL STYLES FOR FORMS */
/* FORM GROUPS */
.form-section-title {
color: var(--color-fontsec);
margin: 0;
+ .form-group {
margin-top: 11px;
}
}
.form-group {
position: relative;
display: flex;
display: grid;
grid-template-columns: 1fr 3fr;
grid-gap: 5px;
justify-content: flex-start;
align-items: flex-start;
+ .form-group, + .form-section-legend, + .form-section-notification {
margin-top: 11px;
}
+ .form-section-title {
margin-top: 40px;
}
}
.form-section-legend {
color: var(--color-fontsec);
margin: 7px 0;
}
.form-group__hint, .form-section-title__hint {
color: var(--color-fontsec);
font-size: 0.9rem;
font-weight: 600;
}
.form-section-title__hint {
margin-top: 7px;
+ .form-group {
margin-top: 11px;
}
}
.form-group-label {
font-weight: 600;
padding-top: 6px;
}
.form-group-label__hint {
margin-top: 7px;
color: var(--color-fontsec);
font-size: 0.9rem;
}
.form-group--required .form-group-label__caption::after, .form-group__required-marker::before {
content: ' *';
color: var(--color-error);
font-weight: 600;
}
.form-group--submit .form-group__input {
grid-column: 2;
}
@media (max-width: 768px) {
.form-group--submit .form-group__input {
grid-column: 1;
}
}
.form-group--has-error {
background-color: rgba(255, 0, 0, 0.1);
input, textarea {
border-color: var(--color-error) !important;
}
.form-error {
display: block;
}
}
.form-error {
display: none;
}
@media (max-width: 768px) {
.form-group {
grid-template-columns: 1fr;
align-items: baseline;
margin-top: 17px;
flex-direction: column;
}
}
/* TEXT INPUTS */
input[type='text'],
input[type='search'],
input[type='password'],
input[type='url'],
input[type='number'],
input[type='email'],
input[type*='date'],
input[type*='time'],
select {
/* from bulma.css */
color: #363636;
border-color: #dbdbdb;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
width: 100%;
max-width: 600px;
align-items: center;
border: 1px solid transparent;
border-radius: 4px;
font-size: 1rem;
font-family: var(--font-base);
line-height: 1.5;
padding: 4px 13px;
}
input[type='number'] {
width: 100px;
}
input[type*='date'],
input[type*='time'],
.flatpickr-input[type='text'] {
width: 50%;
width: 250px;
}
/* BUTTON STYLE SEE default-layout.lucius */
/* TEXTAREAS */
textarea {
width: 100%;
height: 170px;
max-width: 600px;
line-height: 1.5;
color: #363636;
background-color: #f3f3f3;
padding: 4px 13px;
font-size: 1rem;
font-family: var(--font-base);
-webkit-appearance: none;
appearance: none;
border: 1px solid #dbdbdb;
border-radius: 2px;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
vertical-align: top;
}
/* SHARED STATE RELATED STYLES */
input,
select,
textarea {
&:focus {
border-color: #3273dc;
box-shadow: 0 0 0 0.125em rgba(50,115,220,.25);
outline: 0;
}
&[disabled] {
background-color: #f3f3f3;
color: #7a7a7a;
box-shadow: none;
border-color: #dbdbdb;
}
&[readonly] {
background-color: #f5f5f5;
border-color: #dbdbdb;
}
}
/* OPTIONS */
select {
-webkit-appearance: menulist;
}
select,
option {
font-size: 1rem;
line-height: 1.5;
padding: 4px 13px;
border: 1px solid #dbdbdb;
border-radius: 2px;
outline: 0;
color: #363636;
min-width: 250px;
width: auto;
background-color: #f3f3f3;
box-shadow: inset 0 1px 2px 1px rgba(50,50,50,.05);
}
@media (max-width: 425px) {
select, option {
width: 100%;
}
}
/* FILE INPUT */
.file-input {
display: none;
}
.file-input__label {
cursor: pointer;
display: inline-block;
background-color: var(--color-primary);
color: white;
padding: 10px 17px;
border-radius: 3px;
}
.file-input__info {
font-size: .9rem;
font-style: italic;
margin: 10px 0;
color: var(--color-fontsec);
}
.file-input__list {
margin-left: 40px;
margin-top: 10px;
font-weight: 600;
}
/* PREVIOUSLY UPLOADED FILES */
.file-uploads-label {
margin-bottom: 10px;
}
.file-container {
display: flex;
align-items: center;
margin-bottom: 10px;
.checkbox {
margin-left: 12px;
}
}

View File

@ -0,0 +1,55 @@
// CUSTOM RADIO BOXES
// Completely replaces native radiobox
.radio-group
display: flex
.radio
position: relative
display: inline-block
[type='radio']
position: fixed
top: -1px
left: -1px
width: 1px
height: 1px
overflow: hidden
label
display: block
height: 34px
min-width: 42px
line-height: 34px
text-align: center
padding: 0 13px
background-color: #f3f3f3
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05)
color: var(--color-font)
cursor: pointer
\:checked + label
background-color: var(--color-primary)
color: var(--color-lightwhite)
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15)
\:focus + label
border-color: #3273dc
box-shadow: 0 0 0.125em 0 rgba(50, 115, 220, 0.8)
outline: 0
[disabled] + label
pointer-events: none
border: none
opacity: 0.6
filter: grayscale(1)
.radio:first-child
label
border-top-left-radius: 4px
border-bottom-left-radius: 4px
.radio:last-child
label
border-top-right-radius: 4px
border-bottom-right-radius: 4px

View File

@ -1,66 +0,0 @@
/* CUSTOM RADIO BOXES */
/* Completely replaces native radiobox */
.radio-group {
display: flex;
}
.radio {
position: relative;
display: inline-block;
[type='radio'] {
position: fixed;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
overflow: hidden;
}
label {
display: block;
height: 34px;
min-width: 42px;
line-height: 34px;
text-align: center;
padding: 0 13px;
background-color: #f3f3f3;
box-shadow: inset 2px 1px 2px 1px rgba(50, 50, 50, 0.05);
color: var(--color-font);
cursor: pointer;
}
:checked + label {
background-color: var(--color-primary);
color: var(--color-lightwhite);
box-shadow: inset -2px -1px 2px 1px rgba(255, 255, 255, 0.15);
}
:focus + label {
border-color: #3273dc;
box-shadow: 0 0 0.125em 0 rgba(50,115,220,0.8);
outline: 0;
}
[disabled] + label {
pointer-events: none;
border: none;
opacity: 0.6;
filter: grayscale(1);
}
}
.radio:first-child {
label {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
.radio:last-child {
label {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}

View File

@ -1,6 +1,6 @@
import { Utility } from '../../core/utility';
import { Datepicker } from '../form/datepicker';
import './mass-input.scss';
import './mass-input.sass';
const MASS_INPUT_CELL_SELECTOR = '.massinput__cell';
const MASS_INPUT_ADD_CELL_SELECTOR = '.massinput__cell--add';

View File

@ -0,0 +1,14 @@
.massinput-list__wrapper, .massinput-list__cell
display: grid
grid: auto / auto 50px
max-width: 600px
grid-gap: 7px
.massinput-list__field
grid-column: 1
.massinput-list__add, .massinput-list__delete
grid-column: 2
.massinput-list__cell
grid-column: 1 / 3

View File

@ -1,18 +0,0 @@
.massinput-list__wrapper, .massinput-list__cell {
display: grid;
grid: auto / auto 50px;
max-width: 600px;
grid-gap: 7px;
}
.massinput-list__field {
grid-column: 1;
}
.massinput-list__add, .massinput-list__delete {
grid-column: 2;
}
.massinput-list__cell {
grid-column: 1 / 3;
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './modal.scss';
import './modal.sass';
const MODAL_HEADERS = {
'Is-Modal': 'True',

View File

@ -0,0 +1,103 @@
@use "../../common" as *
.modals-wrapper
position: fixed
left: 0
top: 0
width: 100%
height: 100%
z-index: -1
display: flex
align-items: center
justify-content: center
&.modals-wrapper--open
z-index: 200
width: 100%
height: 100%
.modal
position: relative
display: none
background-color: rgba(255, 255, 255, 1)
min-width: 60vw
max-width: 70vw
min-height: 100px
max-height: calc(100vh - 30px)
border-radius: 2px
z-index: -1
color: var(--color-font)
overflow: auto
overscroll-behavior: contain
pointer-events: none
opacity: 0
&.modal--open
display: flex
opacity: 1
pointer-events: auto
z-index: 200
transition: opacity .2s .1s ease-in-out, transform .3s ease-in-out
@media (max-width: 1024px)
.modal
min-width: 80vw
@media (max-width: 768px)
.modal
min-width: 90vw
@media (max-width: 425px)
.modal
min-width: calc(100vw - 20px)
.modal__overlay
position: fixed
left: 0
top: 0
height: 100%
width: 100%
background-color: transparent
z-index: -1
transition: all .2s ease
display: none
&.modal__overlay--open
display: block
z-index: 199
opacity: 1
background-color: rgba(0, 0, 0, 0.4)
.modal__trigger
cursor: pointer
div.modal__trigger
display: inline-block
.modal__trigger-label
font-style: italic
text-decoration: underline
.modal__closer
position: absolute
top: 20px
right: 20px
display: flex
align-items: center
justify-content: center
width: 30px
height: 30px
background-color: var(--color-darker)
border-radius: 2px
cursor: pointer
z-index: 20
&::before
@extend .fas
content: fa-content($fa-var-times)
color: white
.modal__content
margin: 20px 40px
width: 100%

View File

@ -1,118 +0,0 @@
.modals-wrapper {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: -1;
display: flex;
align-items: center;
justify-content: center;
&.modals-wrapper--open {
z-index: 200;
width: 100%;
height: 100%;
}
}
.modal {
position: relative;
display: none;
background-color: rgba(255, 255, 255, 1);
min-width: 60vw;
max-width: 70vw;
min-height: 100px;
max-height: calc(100vh - 30px);
border-radius: 2px;
z-index: -1;
color: var(--color-font);
overflow: auto;
overscroll-behavior: contain;
pointer-events: none;
opacity: 0;
&.modal--open {
display: flex;
opacity: 1;
pointer-events: auto;
z-index: 200;
transition:
opacity .2s .1s ease-in-out,
transform .3s ease-in-out;
}
}
@media (max-width: 1024px) {
.modal {
min-width: 80vw;
}
}
@media (max-width: 768px) {
.modal {
min-width: 90vw;
}
}
@media (max-width: 425px) {
.modal {
min-width: calc(100vw - 20px);
}
}
.modal__overlay {
position: fixed;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: transparent;
z-index: -1;
transition: all .2s ease;
display: none;
&.modal__overlay--open {
display: block;
z-index: 199;
opacity: 1;
background-color: rgba(0, 0, 0, 0.4);
}
}
.modal__trigger {
cursor: pointer;
}
div.modal__trigger {
display: inline-block;
}
.modal__trigger-label {
font-style: italic;
text-decoration: underline;
}
.modal__closer {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background-color: var(--color-darker);
border-radius: 2px;
cursor: pointer;
z-index: 20;
&::before {
content: '\f00d';
font-family: 'Font Awesome 5 Free';
color: white;
}
}
.modal__content {
margin: 20px 40px;
width: 100%;
}

View File

@ -0,0 +1,48 @@
import { Utility } from '../../core/utility';
import './navbar.sass';
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,
];

View File

@ -0,0 +1,228 @@
.navbar-container
position: relative
.navbar-shadow
position: fixed
right: 0
top: 0
height: var(--header-height-collapsed)
width: 20px
z-index: 50
background-image: linear-gradient(to left, rgba(0, 0, 0, 0.4), transparent)
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
@media (min-width: 768px)
.navbar-shadow
height: var(--header-height)
@media (min-width: 1025px)
.navbar-shadow
display: none
.navbar
position: fixed
display: flex
flex-direction: row
align-items: center
justify-content: flex-start
right: 0
top: 0
left: var(--asidenav-width-xl)
height: var(--header-height)
background-color: var(--color-primary)
color: white
z-index: 20
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2)
overflow: auto
transition: all 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
@media (max-width: 1199px)
.navbar
left: var(--asidenav-width-lg)
@media (max-width: 768px)
.navbar
left: 0
// links
.navbar__link-wrapper
display: flex
flex-direction: column
justify-content: flex-end
align-items: center
height: 80px
min-width: 90px
color: var(--color-lightwhite)
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
overflow: hidden
cursor: pointer
.navbar__link-icon
opacity: 0.7
transition: opacity 0.2s ease
margin-bottom: 7px
.navbar__link-label
transition: opacity .2s ease
padding: 2px 4px
text-transform: uppercase
font-weight: 600
@media (min-width: 769px)
.navbar__link-wrapper
border: 1px solid rgba(255, 255, 255, 0.7)
@media (max-width: 768px)
.navbar__link-wrapper
box-shadow: none
min-width: 0
align-items: center
justify-content: center
.navbar__link-label
padding: 0 7px
.navbar__link-icon
transform: scale(0.65)
margin-bottom: 0
// navbar list
.navbar__list
white-space: nowrap
+ .navbar__list
margin-left: 12px
@media (min-width: 769px)
.navbar__list:last-of-type
padding-right: 40px
@media (max-width: 768px)
.navbar__list
+ .navbar__list
margin-left: 0
padding-right: 40px
// list item
.navbar__list-item
position: relative
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
margin-left: 12px
@media (max-width: 500px)
.navbar__list-item
min-width: 60px
&:not(.navbar__list-item--favorite) + .navbar__list-item
margin-left: 0
&:not(.navbar__list-item--favorite) + .navbar__list-item--lang-wrapper
margin-left: 0
.navbar__list-left
flex: 5
padding-left: 40px
@media (max-width: 768px)
.navbar__list-left
padding-left: 0
// "Favorites" list item, only visible on small screens and logged in
.navbar__list-item
&.navbar__list-item--favorite
display: none
.navbar__list-item--favorite
display: none
background-color: var(--color-primary)
.logged-in
.navbar__list
li.navbar__list-item--favorite,
.navbar__list-item--favorite
display: inline-block
@media (min-width: 426px)
.logged-in
.navbar__list
.navbar__list-item--favorite
display: none !important
.navbar__list-item--active
background-color: var(--color-lightwhite)
color: var(--color-dark)
.navbar__link-wrapper
color: var(--color-dark)
.navbar__list-item--active .navbar__link-wrapper
color: var(--color-dark)
.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)
color: var(--color-lightwhite)
.navbar__link-icon
opacity: 1
// sticky state
.navbar--sticky
height: var(--header-height-collapsed)
z-index: 100
.navbar__link-wrapper
height: var(--header-height-collapsed)
.navbar__pushdown
height: var(--header-height)
transition: height 0.2s cubic-bezier(0.03, 0.43, 0.58, 1)
.navbar--sticky + .navbar__pushdown
display: block
height: var(--header-height-collapsed)
@media (max-width: 768px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
height: var(--header-height-collapsed)
@media (max-height: 500px)
.navbar,
.navbar__pushdown
height: var(--header-height-collapsed)
.navbar__link-wrapper
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)

View File

@ -1,5 +1,6 @@
import { Utility } from '../../core/utility';
import './show-hide.scss';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import './show-hide.sass';
const SHOW_HIDE_LOCAL_STORAGE_KEY = 'SHOW_HIDE';
const SHOW_HIDE_INITIALIZED_CLASS = 'show-hide--initialized';
@ -15,6 +16,8 @@ export class ShowHide {
_showHideId;
_element;
_storageManager = new StorageManager(SHOW_HIDE_LOCAL_STORAGE_KEY, '1.0.0', { location: LOCATION.LOCAL });
constructor(element) {
if (!element) {
throw new Error('ShowHide utility cannot be setup without an element!');
@ -41,9 +44,9 @@ export class ShowHide {
}
if (this._showHideId) {
let localStorageCollapsed = this._getLocalStorage()[this._showHideId];
if (typeof localStorageCollapsed !== 'undefined') {
collapsed = localStorageCollapsed;
let storageCollapsed = this._storageManager.load(this._showHideId);
if (typeof storageCollapsed !== 'undefined') {
collapsed = storageCollapsed;
}
}
this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS, collapsed);
@ -70,18 +73,7 @@ export class ShowHide {
const newState = this._element.parentElement.classList.toggle(SHOW_HIDE_COLLAPSED_CLASS);
if (this._showHideId) {
this._setLocalStorage(this._showHideId, newState);
this._storageManager.save(this._showHideId, newState);
}
}
// maybe move these to a LocalStorageHelper?
_setLocalStorage(id, state) {
const lsData = this._getLocalStorage();
lsData[id] = state;
window.localStorage.setItem(SHOW_HIDE_LOCAL_STORAGE_KEY, JSON.stringify(lsData));
}
_getLocalStorage() {
return JSON.parse(window.localStorage.getItem(SHOW_HIDE_LOCAL_STORAGE_KEY)) || {};
}
}

View File

@ -0,0 +1,44 @@
$show-hide-toggle-size: 6px
.show-hide__toggle
position: relative
cursor: pointer
&:hover
background-color: var(--color-grey-lighter)
cursor: pointer
.show-hide__toggle::before
content: ''
position: absolute
width: $show-hide-toggle-size
height: $show-hide-toggle-size
left: -15px
top: 50%
color: var(--color-primary)
border-right: 2px solid currentColor
border-top: 2px solid currentColor
transition: transform .2s ease
transform: translateY(-50%) rotate(-45deg)
@media (max-width: 768px)
left: auto
right: 20px
color: var(--color-font)
.show-hide__toggle--right::before
left: auto
right: 20px
color: var(--color-font)
.show-hide--collapsed
.show-hide__toggle::before
transform: translateY(-50%) rotate(135deg)
& > :not(.show-hide__toggle)
display: block
height: 0
margin: 0
padding: 0
max-height: 0
overflow-y: hidden

View File

@ -1,54 +0,0 @@
$show-hide-toggle-size: 6px;
.show-hide__toggle {
position: relative;
cursor: pointer;
&:hover {
background-color: var(--color-grey-lighter);
cursor: pointer;
}
}
.show-hide__toggle::before {
content: '';
position: absolute;
width: $show-hide-toggle-size;
height: $show-hide-toggle-size;
left: -15px;
top: 50%;
color: var(--color-primary);
border-right: 2px solid currentColor;
border-top: 2px solid currentColor;
transition: transform .2s ease;
transform: translateY(-50%) rotate(-45deg);
@media (max-width: 768px) {
left: auto;
right: 20px;
color: var(--color-font);
}
}
.show-hide__toggle--right::before {
left: auto;
right: 20px;
color: var(--color-font);
}
.show-hide--collapsed {
.show-hide__toggle::before {
transform: translateY(-50%) rotate(135deg);
}
:not(.show-hide__toggle) {
display: block;
height: 0;
margin: 0;
padding: 0;
max-height: 0;
overflow-y: hidden;
}
}

View File

@ -1,4 +1,4 @@
import './tabber.scss';
import './tabber.sass';
(function($) {

View File

@ -0,0 +1,35 @@
.tab-group
border-top: 2px solid #dcdcdc
padding-top: 30px
.tab-group-openers
display: flex
justify-content: stretch
line-height: 40px
font-size: 14px
margin-bottom: 40px
.tab-opener
display: inline-block
flex: 1
text-align: center
padding: 0 13px
margin: 0 2px
background-color: var(--color-dark)
color: white
font-size: 16px
text-transform: uppercase
font-weight: 600
transition: all .1s ease
border-bottom: 5px solid rgba(100, 100, 100, 0.2)
.tab-opener:not(.tab-visible):hover
cursor: pointer
background-color: transparent
color: rgb(52, 48, 58)
border-bottom-color: grey
.tab-opener.tab-visible
background-color: transparent
color: rgb(52, 48, 58)
border-bottom-color: var(--color-primary)

View File

@ -1,39 +0,0 @@
.tab-group {
border-top: 2px solid #dcdcdc;
padding-top: 30px;
}
.tab-group-openers {
display: flex;
justify-content: stretch;
line-height: 40px;
font-size: 14px;
margin-bottom: 40px;
}
.tab-opener {
display: inline-block;
flex: 1;
text-align: center;
padding: 0 13px;
margin: 0 2px;
background-color: var(--color-dark);
color: white;
font-size: 16px;
text-transform: uppercase;
font-weight: 600;
transition: all .1s ease;
border-bottom: 5px solid rgba(100, 100, 100, 0.2);
}
.tab-opener:not(.tab-visible):hover {
cursor: pointer;
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: grey;
}
.tab-opener.tab-visible {
background-color: transparent;
color: rgb(52, 48, 58);
border-bottom-color: var(--color-primary);
}

View File

@ -1,5 +1,5 @@
import { Utility } from '../../core/utility';
import './tooltips.scss';
import './tooltips.sass';
// empty 'shell' to be able to load styles
@Utility({

View File

@ -0,0 +1,92 @@
.tooltip
position: relative
display: inline-block
vertical-align: middle
&:hover .tooltip__content
display: inline-block
.tooltip__handle
color: var(--color-light)
height: 1.5rem
line-height: 1.5rem
font-size: 1.2rem
display: inline-block
text-align: center
margin: 0 10px
cursor: default
position: relative
&::before
position: absolute
top: 0
left: 0
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 15px
&.tooltip__handle.urgency__success
color: var(--color-success)
&.tooltip__handle.urgency__warning
color: var(--color-warning)
&.tooltip__handle.urgency__error
color: var(--color-error)
&:hover
color: var(--color-dark)
&.tooltip__handle.urgency__success
color: var(--color-success-dark)
&.tooltip__handle.urgency__warning
color: var(--color-warning-dark)
&.tooltip__handle.urgency__error
color: var(--color-error-dark)
.tooltip.tooltip__inline
.tooltip__handle
height: 1.0rem
line-height: 1.0rem
font-size: 1.0rem
.tooltip__content
position: absolute
display: none
top: -10px
transform: translateY(-100%)
left: 3px
width: 275px
z-index: 10
background-color: #fafafa
border-radius: 4px
padding: 13px 17px
box-shadow: 0 0 20px 4px rgba(0, 0, 0, 0.1)
&::after
content: ''
width: 16px
height: 16px
background-color: #fafafa
transform: rotate(45deg)
position: absolute
left: 10px
bottom: -8px
@media (max-width: 768px)
.tooltip
display: block
margin-top: 10px
.tooltip__content
left: 3px
right: 3px
width: auto
// fix font color when used in tableheaders
th .tooltip__content
color: var(--color-font)
font-weight: normal

View File

@ -1,109 +0,0 @@
.tooltip {
position: relative;
display: inline-block;
vertical-align: middle;
&:hover .tooltip__content {
display: inline-block;
}
}
.tooltip__handle {
color: var(--color-light);
height: 1.5rem;
line-height: 1.5rem;
font-size: 1.2rem;
display: inline-block;
text-align: center;
margin: 0 10px;
cursor: default;
position: relative;
&::before {
position: absolute;
top: 0;
left: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 15px;
}
&.tooltip__handle.urgency__success {
color: var(--color-success);
}
&.tooltip__handle.urgency__warning {
color: var(--color-warning);
}
&.tooltip__handle.urgency__error {
color: var(--color-error);
}
&:hover {
color: var(--color-dark);
&.tooltip__handle.urgency__success {
color: var(--color-success-dark);
}
&.tooltip__handle.urgency__warning {
color: var(--color-warning-dark);
}
&.tooltip__handle.urgency__error {
color: var(--color-error-dark);
}
}
}
.tooltip.tooltip__inline {
.tooltip__handle {
height: 1.0rem;
line-height: 1.0rem;
font-size: 1.0rem;
}
}
.tooltip__content {
position: absolute;
display: none;
top: -10px;
transform: translateY(-100%);
left: 3px;
width: 275px;
z-index: 10;
background-color: #fafafa;
border-radius: 4px;
padding: 13px 17px;
box-shadow: 0 0 20px 4px rgba(0, 0, 0, 0.1);
&::after {
content: '';
width: 16px;
height: 16px;
background-color: #fafafa;
transform: rotate(45deg);
position: absolute;
left: 10px;
bottom: -8px;
}
}
@media (max-width: 768px) {
.tooltip {
display: block;
margin-top: 10px;
.tooltip__content {
left: 3px;
right: 3px;
width: auto;
}
}
}
/* fix font color when used in tableheaders */
th .tooltip__content {
color: var(--color-font);
font-weight: normal;
}

View File

@ -10,6 +10,8 @@ import { MassInput } from './mass-input/mass-input';
import { Modal } from './modal/modal';
import { Tooltip } from './tooltips/tooltips';
import { CourseTeaser } from './course-teaser/course-teaser';
import { NavbarUtils } from './navbar/navbar';
import { HideColumns } from './hide-columns/hide-columns';
export const Utils = [
Alerts,
@ -25,4 +27,6 @@ export const Utils = [
ShowHide,
Tooltip,
CourseTeaser,
...NavbarUtils,
HideColumns,
];

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
import './fontawesome.css';
import './datetime.css';

View File

@ -44,7 +44,7 @@ module.exports = function(config) {
}
},
{
test: /\.(css|scss)$/i,
test: /\.(css|scss|sass)$/i,
loader:'null-loader',
}
]

3
messages/button/en.msg Normal file
View File

@ -0,0 +1,3 @@
AmbiguousButtons: Multiple active submit buttons
WrongButtonValue: Submit button has wrong value
MultipleButtonValues: Submit button has multiple values

View File

@ -1,5 +1,6 @@
CampusIdentPlaceholder: Vorname.Nachname@campus.lmu.de
CampusIdent: Campus-Kennung
CampusPassword: Passwort
CampusPasswordPlaceholder: Passwort
CampusSubmit: Abschicken
CampusInvalidCredentials: Ungültige Logindaten

6
messages/campus/en.msg Normal file
View File

@ -0,0 +1,6 @@
CampusIdentPlaceholder: First.Last@campus.lmu.de
CampusIdent: Campus account
CampusPassword: Password
CampusPasswordPlaceholder: Password
CampusSubmit: Send
CampusInvalidCredentials: Invalid login

View File

@ -1,2 +1,3 @@
DummyIdent: Nutzer-Kennung
DummyIdent: Identifikation
DummyIdentPlaceholder: Identifikation
DummyNoFormData: Keine Formulardaten empfangen

3
messages/dummy/en.msg Normal file
View File

@ -0,0 +1,3 @@
DummyIdent: Identification
DummyIdentPlaceholder: Identification
DummyNoFormData: No form data received

View File

@ -1,4 +1,4 @@
FilesSelected: Dateien ausgewählt
SelectFile: Datei 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
View 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!

View File

@ -1,2 +1,4 @@
PWHashIdent: Identifikation
PWHashPassword: Passwort
PWHashIdentPlaceholder: Identifikation
PWHashPassword: Passwort
PWHashPasswordPlaceholder: Passwort

4
messages/pw-hash/en.msg Normal file
View File

@ -0,0 +1,4 @@
PWHashIdent: Identification
PWHashIdentPlaceholder: Identification
PWHashPassword: Password
PWHashPasswordPlaceholder: Password

View File

@ -1,5 +1,7 @@
PrintDebugForStupid name@Text: Debug message "#{name}"
Logo: Uni2work
BtnSubmit: Senden
BtnAbort: Abbrechen
BtnDelete: Löschen
@ -15,9 +17,13 @@ BtnHijack: Sitzung übernehmen
BtnSave: Speichern
PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert.
BtnHandIn: Abgeben
BtnCandidatesInfer: Studienfachzuordnung automatisch lernen
BtnCandidatesDeleteConflicts: Konflikte löschen
BtnCandidatesDeleteAll: Alle Beobachtungen löschen
BtnNameCandidatesInfer: Studienfach-Namens-Zuordnung automatisch lernen
BtnNameCandidatesDeleteConflicts: Namenskonflikte löschen
BtnNameCandidatesDeleteAll: Alle Namens-Beobachtungen löschen
BtnParentCandidatesInfer: Unterstudiengangs-Zuordnung automatisch lernen
BtnParentCandidatesDeleteAll: Alle Unterstudiengangs-Beobachtungen löschen
BtnStandaloneCandidatesDeleteAll: Alle Einzelstudiengangs-Beobachtungen löschen
BtnStandaloneCandidatesDeleteRedundant: Redundante Einzelstudiengangs-Beobachtungen löschen
BtnResetTokens: Authorisierungs-Tokens invalidieren
BtnLecInvAccept: Annehmen
BtnLecInvDecline: Ablehnen
@ -30,6 +36,7 @@ Aborted: Abgebrochen
Remarks: Hinweise
Registered: Angemeldet
RegisteredSince: Angemeldet seit
NotRegistered: Sie sind zu diesem Kurs nicht angemeldet.
Registration: Anmeldung
RegisterFrom: Anmeldungen von
RegisterTo: Anmeldungen bis
@ -70,9 +77,9 @@ Term: Semester
TermPlaceholder: W/S + vierstellige Jahreszahl
TermStartDay: Erster Tag
TermStartDayTooltip: Üblicherweise immer 1.April oder 1.Oktober
TermStartDayTooltip: Üblicherweise immer 1. April oder 1. Oktober
TermEndDay: Letzter Tag
TermEndDayTooltip: Üblicherweise immer 30.September oder 31.März
TermEndDayTooltip: Üblicherweise immer 30. September oder 31. März
TermHolidays: Feiertage
TermHolidayPlaceholder: Feiertag
TermLectureStart: Beginn Vorlesungen
@ -97,6 +104,7 @@ CourseRegistration: Kursanmeldung
CourseRegisterOpen: Anmeldung möglich
CourseRegisterOk: Erfolgreich zum Kurs angemeldet
CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet
CourseApply: Zum Kurs bewerben
CourseApplyOk: Erfolgreich zum Kurs beworben
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
@ -108,8 +116,8 @@ CourseTutorial: Tutorium
CourseSecretWrong: Falsches Passwort
CourseSecret: Zugangspasswort
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.
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.
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 und Institut.
FFSheetName: Name
TermCourseListHeading tid@TermId: Kursübersicht #{tid}
TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{tid} für #{school}
@ -127,7 +135,7 @@ CourseMembersCountLimited n@Int max@Int: #{n}/#{max}
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"}
CourseName: Name
CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
CourseDescriptionTip: Beliebiges Html-Markup ist gestattet
CourseHomepageExternal: Externe Homepage
CourseShorthand: Kürzel
CourseShorthandUnique: Muss nur innerhalb Institut und Semester eindeutig sein. Wird verbatim in die Url der Kursseite übernommen.
@ -141,15 +149,20 @@ CourseRegisterToTip: Darf auch unbegrenzt offen bleiben
CourseDeregisterUntilTip: Abmeldung ist ab "Anmeldungen von" bis zu diesem Zeitpunkt erlaubt. Die Abmeldung darf auch unbegrenzt erlaubt bleiben.
CourseFilterSearch: Volltext-Suche
CourseFilterRegistered: Registriert
CourseFilterNone: Egal
CourseFilterNone: —
BoolIrrelevant: —
CourseDeleteQuestion: Wollen Sie den unten aufgeführten Kurs wirklich löschen?
CourseDeleted: Kurs gelöscht
CourseUserTutorials: Angemeldete Tutorien
CourseUserNote: Notiz
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
CourseUserNoteTooltip: Nur für Verwalter dieses Kurses einsehbar
CourseUserNoteSaved: Notizänderungen gespeichert
CourseUserNoteDeleted: Teilnehmernotiz gelöscht
CourseUserRegister: Zum Kurs anmelden
CourseUserDeregister: Vom Kurs abmelden
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
CourseUserRegisterTutorial: Zu einem Tutorium anmelden
CourseUsersTutorialRegistered count@Int64: #{show count} Teilnehmer zum Tutorium angemeldet
CourseUserSendMail: Mitteilung verschicken
TutorialUserDeregister: Vom Tutorium Abmelden
TutorialUserSendMail: Mitteilung verschicken
@ -180,7 +193,7 @@ CourseApplication: Bewerbung
CourseApplicationIsParticipant: Kursteilnehmer
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
CourseApplicationEdited csh@CourseShorthand: Bewerbung zu #{csh} erfolgreich angepasst
CourseApplicationNotEdited csh@CourseShorthand: Bewerbung zu #{csh} hat sich nicht verändert
@ -267,6 +280,8 @@ SheetSubmissionMode: Abgabe-Modus
SheetExercise: Aufgabenstellung
SheetHint: Hinweis
SheetHintFrom: Hinweis ab
SheetHintFromPlaceholder: Datum, sonst nur für Korrektoren
SheetSolutionFromPlaceholder: Datum, sonst nur für Korrektoren
SheetSolution: Lösung
SheetSolutionFrom: Lösung ab
SheetMarking: Hinweise für Korrektoren
@ -278,9 +293,15 @@ SheetDescription: Hinweise für Teilnehmer
SheetGroup: Gruppenabgabe
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
SheetActiveFrom: Beginn Abgabezeitraum
SheetActiveFromTip: Download der Aufgabenstellung erst ab diesem Datum möglich
SheetActiveTo: Ende Abgabezeitraum
SheetActiveFrom: Aktiv ab/Beginn Abgabezeitraum
SheetActiveFromParticipant: Beginn 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
SheetSolutionFromTip: Ohne Datum nie für Teilnehmer sichtbar, Korrektoren können diese Dateien immer herunterladen
SheetMarkingTip: Hinweise zur Korrektur, sichtbar nur für Korrektoren
@ -311,6 +332,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
SubmissionMembers: Abgebende
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)
SubmissionFile: Datei zur Abgabe
SubmissionFiles: Abgegebene Dateien
@ -322,6 +344,8 @@ NoOpenSubmissions: Keine unkorrigierten Abgaben vorhanden
SubmissionsDeleteQuestion n@Int: Wollen Sie #{pluralDE n "die unten aufgeführte Abgabe" "die unten aufgeführten Abgaben"} wirklich löschen?
SubmissionsDeleted n@Int: #{pluralDE n "Abgabe gelöscht" "Abgaben gelöscht"}
SubmissionDeleteCosubmittorsWarning n@Int: Bei #{pluralDE n "der oben aufgeführte Abgabe" "einer der oben aufgeführten Abgaben"} gibt es, außer Ihnen, noch weitere Mitabgeber. Stellen Sie sicher, dass sie Abgaben nur in Absprache mit Ihren Mitabgebern löschen oder verlassen Sie die Abgabe, indem Sie sich selbst aus der Liste der Abgebenden entfernen!
SubmissionGroupName: Gruppenname
CorrectionsTitle: Zugewiesene Korrekturen
@ -329,6 +353,8 @@ CourseCorrectionsTitle: Korrekturen für diesen Kurs
CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName}
CorrectorAssignTitle: Korrektor zuweisen
CorrectionsGrade: Korrekturen eintragen
MaterialName: Name
MaterialType: Art
MaterialTypePlaceholder: Folien, Code, Beispiel, ...
@ -366,6 +392,8 @@ UnauthorizedTokenNotStarted: Ihr Authorisierungs-Token ist noch nicht gültig.
UnauthorizedTokenInvalid: Ihr Authorisierungs-Token konnte nicht verarbeitet werden.
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.
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.
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
@ -377,12 +405,15 @@ UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung e
UnauthorizedAllocationLecturer: Sie sind nicht als Veranstalter für eine Veranstaltung dieser Zentralanmeldung eingetragen.
UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung eingetragen.
UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
UnauthorizedExamCorrector: Sie sind nicht als Korrektor für diese Prüfung eingetragen.
UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen.
UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert.
UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert.
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
UnauthorizedCourseNewsParticipant: Sie sind kein Teilnehmer dieser Veranstaltung.
UnauthorizedParticipantSelf: Sie sind kein Teilnehmer dieser Veranstaltung.
UnauthorizedApplicant: Angegebener Benutzer hat sich nicht für diese Veranstaltung beworben.
UnauthorizedApplicantSelf: Sie sind kein Bewerber für diese Veranstaltung.
UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
UnauthorizedAllocationRegisterTime: Diese Zentralanmeldung erlaubt momentan keine Bewerbungen.
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
@ -409,6 +440,7 @@ UnknownAuthPredicate tag@String: Authorisierungsprädikat "#{tag}" ist dem Syste
UnauthorizedRedirect: Die angeforderte Seite existiert nicht oder Sie haben keine Berechtigung, die angeforderte Seite zu sehen.
UnauthorizedSelf: Aktueller Nutzer ist nicht angegebener Benutzer.
UnauthorizedTutorialTutor: Sie sind nicht Tutor für dieses Tutorium.
UnauthorizedTutorialTutorControl: Tutoren dürfen dieses Tutorium nicht editieren.
UnauthorizedCourseTutor: Sie sind nicht Tutor für diesen Kurs.
UnauthorizedTutor: Sie sind nicht Tutor.
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Tutorium mit derselben Registrierungs-Gruppe.
@ -459,8 +491,7 @@ ProfileFor: Benutzereinstellungen für
ProfileDataHeading: Gespeicherte Benutzerdaten
InfoHeading: Informationen
VersionHeading: Versionsgeschichte
ImpressumHeading: Impressum
DataProtHeading: Datenschutzerklärung
LegalHeading: Rechtliche Informationen
SystemMessageHeading: Uni2work Statusmeldung
SystemMessageListHeading: Uni2work Statusmeldungen
NotificationSettingsHeading displayName@Text: Benachrichtigungs-Einstellungen für #{displayName}
@ -471,7 +502,7 @@ HomeOpenAllocations: Offene Zentralanmeldungen
HomeUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} Kurse
NumCourses num@Int64: #{num} #{pluralDE num "Kurs" "Kurse"}
CloseAlert: Schliessen
Name: Name
@ -512,15 +543,15 @@ NatField name@Text: #{name} muss eine natürliche Zahl sein!
JSONFieldDecodeFailure aesonFailure@String: Konnte JSON nicht parsen: #{aesonFailure}
SecretJSONFieldDecryptFailure: Konnte versteckte vertrauliche Daten nicht entschlüsseln
SubmissionsAlreadyAssigned num@Int64: #{num} 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):
UpdatedAssignedCorrectorSingle num@Int64: #{num} Abgaben wurden dem neuen Korrektor zugeteilt.
SubmissionsAlreadyAssigned num@Int64: #{num} #{pluralDE num "Abgabe" "Abgaben"} waren bereits einem Korrektor zugeteilt und wurden nicht verändert:
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} #{pluralDE num "Abgabe" "Abgaben"} wurden dem neuen Korrektor zugeteilt.
NoCorrector: Kein Korrektor
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} Abgaben entfernt.
UpdatedAssignedCorrectorsAuto num@Int64: #{num} Abgaben wurden unter den Korrektoren aufgeteilt.
RemovedCorrections num@Int64: Korrektur-Daten wurden von #{num} #{pluralDE num "Abgabe" "Abgaben"} entfernt.
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.
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!
SubmissionOriginal: Original
@ -597,20 +628,21 @@ RatingNotUnicode uexc@UnicodeException: Bewertungsdatei nicht in UTF-8 kodiert:
RatingMissingSeparator: Präambel der Bewertungsdatei konnte nicht identifziert werden
RatingMultiple: Bewertungen enthält mehrere Punktzahlen für die gleiche Abgabe
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
RatingExceedsMax: Bewertung übersteigt die erlaubte Maximalpunktzahl
RatingNotExpected: Keine Bewertungen erlaubt
RatingBinaryExpected: Bewertung muss 0 (=durchgefallen) oder 1 (=bestanden) sein
RatingPointsRequired: Bewertung erfordert für dieses Blatt eine Punktzahl
RatingFile: Bewertungsdatei
SubmissionSinkExceptionDuplicateFileTitle file@FilePath: Dateiname #{show file} kommt mehrfach im Zip-Archiv vor
SubmissionSinkExceptionDuplicateRating: Mehr als eine Bewertung gefunden.
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!
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
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
@ -620,7 +652,7 @@ AdminHeading: Administration
AdminUserHeading: Benutzeradministration
AdminUserRightsHeading: Benutzerrechte
AdminUserAuthHeading: Benutzer-Authentifizierung
AdminUserHeadingFor: Benuterprofil für
AdminUserHeadingFor: Benutzerprofil für
AdminFor: Administrator
LecturerFor: Dozent
LecturersFor: Dozenten
@ -628,7 +660,6 @@ AssistantFor: Assistent
AssistantsFor: Assistenten
TutorsFor n@Int: #{pluralDE n "Tutor" "Tutoren"}
CorrectorsFor n@Int: #{pluralDE n "Korrektor" "Korrektoren"}
ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungen erfolgreich verändert
AccessRightsNotChanged: Berechtigungen wurden nicht verändert
@ -640,7 +671,7 @@ DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat
TimeFormat: Uhrzeitformat
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
WarningDaysTip: Wie viele Tage im Voraus sollen Fristen von Klausuren etc. auf Ihrer Startseite angezeigt werden?
NotificationSettings: Erwünschte Benachrichtigungen
@ -652,6 +683,10 @@ FormCosmetics: Oberfläche
FormPersonalAppearance: Öffentliche Daten
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
InvalidDateTimeFormat: Ungültiges Datums- und Zeitformat, JJJJ-MM-TTTHH:MM[:SS] Format erwartet
@ -683,6 +718,7 @@ CampusUserInvalidTitle: Konnte anhand des Campus-Logins keinen akademischen Tite
CampusUserInvalidMatriculation: Konnte anhand des Campus-Logins keine Matrikelnummer ermitteln
CampusUserInvalidFeaturesOfStudy parseErr@Text: Konnte anhand des Campus-Logins keine Studiengänge ermitteln
CampusUserInvalidAssociatedSchools parseErr@Text: Konnte anhand des Campus-Logins keine Institute ermitteln
CampusUserInvalidSex: Konnte anhand des Campus-Logins kein Geschlecht ermitteln
CorrectorNormal: Normal
CorrectorMissing: Abwesend
@ -717,8 +753,8 @@ UploadSpecificFileRequired: Zur Abgabe erforderlich
NoSubmissions: Keine Abgabe
CorrectorSubmissions: Abgabe extern mit Pseudonym
UserSubmissions: Direkte Abgabe
BothSubmissions: Abgabe direkt & extern mit Pseudonym
UserSubmissions: Direkte Abgabe in Uni2work
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.
@ -730,8 +766,16 @@ SubmissionUpdated: Abgabe erfolgreich ersetzt
AdminFeaturesHeading: Studiengänge
StudyTerms: Studiengänge
StudyTerm: Studiengang
NoStudyTermsKnown: Nicht eingeschrieben
StudyFeatureInference: Studiengangschlüssel-Inferenz
NoStudyTermsKnown: Keine Studiengänge bekannt
StudyFeaturesDegrees: Abschlüsse
StudyFeaturesTerms: Studiengänge
StudyFeaturesNameCandidates: Namens-Kandidaten
StudyFeaturesParentCandidates: Kandidaten für Unterstudiengänge
StudyFeaturesStandaloneCandidates: Kandidaten für Einzelstudiengänge
StudyFeatureNameInference: Studiengangschlüssel-Inferenz
StudyFeatureParentInference: Unterstudiengang-Inferenz
StudyFeatureInferenceNoNameConflicts: Keine Konflikte beobachtet
StudyFeatureInferenceNameConflictsHeading: Studiengangseinträge mit beobachteten Konflikten
StudyFeatureAge: Fachsemester
StudyFeatureDegree: Abschluss
FieldPrimary: Hauptfach
@ -749,14 +793,20 @@ DegreeShort: Abschlusskürzel
StudyTermsKey: Studiengangschlüssel
StudyTermsName: Studiengang
StudyTermsShort: Studiengangkürzel
StudyTermsChangeSuccess: Zuordnung Abschlüsse aktualisiert
StudyDegreeChangeSuccess: Zuordnung Studiengänge aktualisiert
StudyCandidateIncidence: Anmeldevorgang
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
CandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
NoCandidatesInferred: Keine neuen Studienganszuordnungen inferiert
AllIncidencesDeleted: Alle Beobachtungen wurden gelöscht.
StudyTermsChangeSuccess: Zuordnung Studiengänge aktualisiert
StudyDegreeChangeSuccess: Zuordnung Abschlüsse aktualisiert
StudyCandidateIncidence: Synchronisation
AmbiguousNameCandidatesRemoved n@Int: #{show n} #{pluralDE n "uneindeutiger Kandidat" "uneindeutige Kandiaten"} entfernt
RedundantNameCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Namenskandidat" "bekannte Namenskandiaten"} entfernt
RedundantParentCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Elternkandidat" "bekannte Elternkandiaten"} entfernt
RedundantStandaloneCandidatesRemoved n@Int: #{show n} bereits #{pluralDE n "bekannter Einzelstudiengangskandidat" "bekannte Einzelstudiengangskandiaten"} entfernt
NameCandidatesInferred n@Int: #{show n} neue #{pluralDE n "Studiengangszuordnung" "Studiengangszuordnungen"} inferiert
NoNameCandidatesInferred: Keine neuen Studienganszuordnungen inferiert
ParentCandidatesInferred n@Int: #{show n} #{pluralDE n "neuer Unterstudiengang" "neue Unterstudiengänge"} inferiert
NoParentCandidatesInferred: Keine neuen Unterstudiengänge inferiert
AllNameIncidencesDeleted: Alle Namens-Beobachtungen wurden gelöscht.
AllParentIncidencesDeleted: Alle Unterstudiengang-Beobachtungen wurden gelöscht.
AllStandaloneIncidencesDeleted: Alle Einzelstudiengang-Beobachtungen wurden gelöscht.
IncidencesDeleted n@Int: #{show n} #{pluralDE n "Beobachtung" "Beobachtungen"} gelöscht
StudyTermIsNew: Neu
StudyFeatureConflict: Es wurden Konflikte in der Studiengang-Zuordnung gefunden
@ -770,10 +820,27 @@ MailTestDateTime: Test der Datumsformattierung:
German: Deutsch
GermanGermany: Deutsch (Deutschland)
English: Englisch
EnglishEurope: Englisch (Europa)
MailSubjectSubmissionRated csh@CourseShorthand: Ihre #{csh}-Abgabe wurde korrigiert
MailSubmissionRatedIntro courseName@Text termDesc@Text: Ihre Abgabe im Kurs #{courseName} (#{termDesc}) wurde korrigiert.
MailSubjectSubmissionEdited csh@CourseShorthand shn@SheetName: Ihre Abgabe für #{shn} im Kurs #{csh} wurde verändert
MailSubmissionEditedIntro coursen@CourseName shn@SheetName termDesc@Text displayName@Text: #{displayName} hat Ihre Abgabe für #{shn} im Kurs #{coursen} (#{termDesc}) verändert.
MailSubjectSubmissionUserCreated csh@CourseShorthand shn@SheetName: Sie wurden als Mitabgebender zu einer Abgabe für #{shn} im Kurs #{csh} hinzugefügt
MailSubjectSubmissionUserCreatedOther displayName@Text csh@CourseShorthand shn@SheetName: Es wurde ein Mitabgebender zu einer Abgabe für #{shn} im Kurs #{csh} hinzugefügt
MailSubmissionUserCreatedIntro coursen@CourseName shn@SheetName termDesc@Text: Sie wurden als Mitabgebender zu einer Abgabe für #{shn} im Kurs #{coursen} (#{termDesc}) hinzugefügt.
MailSubmissionUserCreatedOtherIntro displayName@UserDisplayName coursen@CourseName shn@SheetName termDesc@Text: #{displayName} wurde als Mitabgebender zu einer Abgabe für #{shn} im Kurs #{coursen} (#{termDesc}) hinzugefügt.
MailSubjectSubmissionUserDeleted csh@CourseShorthand shn@SheetName: Sie wurden als Mitabgebender von Ihrer Abgabe für #{shn} im Kurs #{csh} entfernt
MailSubjectSubmissionUserDeletedOther displayName@Text csh@CourseShorthand shn@SheetName: Es wurde ein Mitabgebender von einer Abgabe für #{shn} im Kurs #{csh} entfernt
MailSubmissionUserDeletedIntro coursen@CourseName shn@SheetName termDesc@Text: Sie wurden als Mitabgebender von Ihrer Abgabe für #{shn} im Kurs #{coursen} (#{termDesc}) entfernt.
MailSubmissionUserDeletedOtherIntro displayName@UserDisplayName coursen@CourseName shn@SheetName termDesc@Text: #{displayName} wurde als Mitabgebender von einer Abgabe für #{shn} im Kurs #{coursen} (#{termDesc}) entfernt.
MailSubjectSheetActive csh@CourseShorthand sheetName@SheetName: #{sheetName} in #{csh} wurde herausgegeben
MailSheetActiveIntro courseName@Text termDesc@Text sheetName@SheetName: Sie können nun #{sheetName} im Kurs #{courseName} (#{termDesc}) herunterladen.
@ -851,9 +918,9 @@ MailSubjectExamOfficeUserInvitation displayName@Text: Berücksichtigung von Prü
MailSubjectPasswordReset: Uni2work-Passwort ändern bzw. setzen
SheetGrading: Bewertung
SheetGradingPoints maxPoints@Points: #{maxPoints} Punkte
SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} Punkten
SheetGradingPassBinary: Bestanden/Nicht Bestanden
SheetGradingPoints maxPoints@Points: #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkte"}
SheetGradingPassPoints maxPoints@Points passingPoints@Points: Bestanden ab #{passingPoints} von #{maxPoints} #{pluralDE maxPoints "Punkt" "Punkten"}
SheetGradingPassBinary: Bestanden/Nicht Bestanden
SheetGradingInfo: "Bestanden nach Punkten" zählt sowohl zur maximal erreichbaren Gesamtpunktzahl also auch zur Anzahl der zu bestehenden Blätter.
SheetGradingCount': Anzahl
@ -907,14 +974,17 @@ NotificationTriggerExamRegistrationSoonInactive: Ich kann mich bald nicht mehr f
NotificationTriggerExamDeregistrationSoonInactive: Ich kann mich bald nicht mehr von einer Prüfung abmelden
NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen
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
NotificationTriggerAllocationOutdatedRatings: Zentralanmeldung-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
NotificationTriggerAllocationOutdatedRatings: Zentralanmeldungs-Bewerbungen für einen meiner Kurse wurden verändert, nachdem sie bewertet wurden
NotificationTriggerAllocationUnratedApplications: Bewertungen zu Zentralanmeldungs-Bewerbungen für einen meiner Kurse stehen aus
NotificationTriggerAllocationResults: Plätze wurden für eine meiner Zentralanmeldungen verteilt
NotificationTriggerExamOfficeExamResults: Ich kann neue Prüfungsergebnisse einsehen
NotificationTriggerExamOfficeExamResultsChanged: Prüfungsergebnisse wurden verändert
NotificationTriggerCourseRegistered: Ein Kursverwalter hat mich zu einem Kurs angemeldet
NotificationTriggerSubmissionUserCreated: Ich wurde als Mitabgebender zu einer Übungsblatt-Abgabe hinzugefügt
NotificationTriggerSubmissionEdited: Eine meiner Übungsblatt-Abgaben wurde verändert
NotificationTriggerSubmissionUserDeleted: Ich wurde als Mitabgebender von einer Übungsblatt-Abgabe entfernt
NotificationTriggerKindAll: Für alle Benutzer
NotificationTriggerKindCourseParticipant: Für Kursteilnehmer
@ -927,8 +997,9 @@ NotificationTriggerKindExamOffice: Für das Prüfungsamt
NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
NotificationTriggerKindSubmissionUser: Für Mitabgebende einer Übungsblatt-Abgabe
CorrCreate: Abgaben erstellen
CorrCreate: Abgaben registrieren
UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}"
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.
@ -948,6 +1019,10 @@ SheetCreateExisting: Folgende Pseudonyme haben bereits abgegeben:
CorrGrade: Korrekturen eintragen
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
HelpAnswer: Antworten an
@ -1020,7 +1095,6 @@ EncodedSecretBoxCouldNotDecodeNonce: Konnte secretbox-nonce nicht dekodieren
EncodedSecretBoxCouldNotOpenSecretBox: Konnte libsodium-secretbox nicht öffnen (Verschlüsselte Daten sind nicht authentisch)
EncodedSecretBoxCouldNotDecodePlaintext aesonErr@String: Konnte Klartext nicht JSON-dekodieren: #{aesonErr}
ErrMsgHeading: Fehlermeldung entschlüsseln
ErrorCryptoIdMismatch: Verschlüsselte Id der Abgabe passte nicht zu anderen Daten
InvalidRoute: Konnte URL nicht interpretieren
@ -1028,8 +1102,11 @@ MenuOpenCourses: Kurse mit offener Registrierung
MenuOpenAllocations: Aktive Zentralanmeldungen
MenuHome: Aktuell
MenuInformation: Informationen
MenuImpressum: Impressum
MenuDataProt: Datenschutz
MenuLegal: Rechtliche Informationen
MenuDataProt: Datenschutzerklärung
MenuTermsUse: Nutzungsbedingungen
MenuCopyright: Urheberrecht
MenuImprint: Impressum
MenuVersion: Versionsgeschichte
MenuInstance: Instanz-Identifikation
MenuHealth: Instanz-Zustand
@ -1057,6 +1134,7 @@ MenuProfileData: Persönliche Daten
MenuTermCreate: Neues Semester anlegen
MenuCourseNew: Neuen Kurs anlegen
MenuTermEdit: Semester editieren
MenuTermCurrent: Aktuelles Semester
MenuCorrection: Korrektur
MenuCorrections: Korrekturen
MenuCorrectionsOwn: Meine Korrekturen
@ -1084,9 +1162,9 @@ MenuSheetClone: Als neues Übungsblatt klonen
MenuCorrectionsUpload: Korrekturen hochladen
MenuCorrectionsDownload: Offene Abgaben herunterladen
MenuCorrectionsCreate: Abgaben registrieren
MenuCorrectionsGrade: Abgaben online korrigieren
MenuCorrectionsAssign: Zuteilung Korrekturen
MenuCorrectionsAssignSheet name@Text: Zuteilung Korrekturen von #{name}
MenuCorrectionsGrade: Korrekturen eintragen
MenuCorrectionsAssign: Zuteilung der Korrekturen
MenuCorrectionsAssignSheet name@Text: Zuteilung der Korrekturen von #{name}
MenuAuthPreds: Authorisierungseinstellungen
MenuTutorialDelete: Tutorium löschen
MenuTutorialEdit: Tutorium editieren
@ -1097,10 +1175,11 @@ MenuExamEdit: Bearbeiten
MenuExamUsers: Teilnehmer
MenuExamGrades: Prüfungsleistungen
MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
MenuExamCorrect: Prüfungsergebnisse eintragen
MenuExamOfficeExams: Prüfungen
MenuExamOfficeFields: Fächer
MenuExamOfficeUsers: Benutzer
MenuLecturerInvite: Dozenten hinzufügen
MenuLecturerInvite: Funktionäre hinzufügen
MenuAllocationInfo: Hinweise zum Ablauf einer Zentralanmeldung
MenuCourseApplicationsFiles: Dateien aller Bewerbungen
MenuSchoolList: Institute
@ -1110,6 +1189,62 @@ MenuCourseNewsEdit: Kursnachricht bearbeiten
MenuCourseEventNew: Neuer Kurstermin
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
BreadcrumbExamCorrect: Eintragen von Prüfungsergebnissen
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.
AuthPredsActive: Aktive Authorisierungsprädikate
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
@ -1122,7 +1257,9 @@ AuthTagDeprecated: Seite ist nicht überholt
AuthTagDevelopment: Seite ist nicht in Entwicklung
AuthTagLecturer: Nutzer ist Dozent
AuthTagCorrector: Nutzer ist Korrektor
AuthTagExamCorrector: Nutzer ist Klausurkorrektor
AuthTagTutor: Nutzer ist Tutor
AuthTagTutorControl: Tutoren haben Kontrolle über ihre Tutorium
AuthTagTime: Zeitliche Einschränkungen sind erfüllt
AuthTagStaffTime: Zeitliche Einschränkungen für Lehrbeteiligte sind erfüllt
AuthTagAllocationTime: Zeitliche Einschränkungen durch Zentralanmeldung sind erfüllt
@ -1132,6 +1269,7 @@ AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
AuthTagExamRegistered: Nutzer ist Prüfungsteilnehmer
AuthTagExamResult: Nutzer hat Prüfungsergebnisse
AuthTagParticipant: Nutzer ist mit Kurs assoziiert
AuthTagApplicant: Nutzer ist mit Bewerber zum Kurs
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
AuthTagCapacity: Kapazität ist ausreichend
AuthTagEmpty: Kurs hat keine Teilnehmer
@ -1164,7 +1302,7 @@ CommBody: Nachricht
CommBodyTip: Das Eingabefeld akzeptiert derzeit ausschließlich Html. U.A. Zeilumbrüche werden dementsprechend ignoriert und müssen manuell mit <br> eingefügt werden.
CommRecipients: Empfänger
CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format and die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format an die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
CommUndisclosedRecipients: Verborgene Empfänger
@ -1186,7 +1324,7 @@ RGTutorialParticipants: Tutorium-Teilnehmer
MultiSelectFieldTip: Mehrfach-Auswahl ist möglich (Umschalt bzw. Strg)
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
LecturerInvitationDeclined csh@CourseShorthand: Sie haben die Einladung, Kursverwalter für #{csh} zu werden, abgelehnt
@ -1259,6 +1397,8 @@ ExceptionExists: Diese Ausnahme existiert bereits
ExceptionNoOccurAt: Termin
TutorialType: Typ
TutorialTypePlaceholder: Tutorium, Zentralübung, ...
TutorialTypeTip: Dient nur der Information der Studierenden
TutorialName: Bezeichnung
TutorialParticipants: Teilnehmer
TutorialCapacity: Kapazität
@ -1273,9 +1413,11 @@ TutorialDeregisterUntil: Abmeldungen bis
TutorialsHeading: Tutorien
TutorialEdit: Bearbeiten
TutorialDelete: Löschen
TutorialTutorControlled: Tutoren dürfen Tutorium editieren
TutorialTutorControlledTip: Sollen Tutoren beliebige Aspekte dieses Tutoriums (Name, Registrierungs-Gruppe, Raum, Zeit, andere Tutoren, ...) beliebig editieren dürfen?
CourseExams: Prüfungen
CourseTutorials: Übungen
CourseTutorials: Tutorien
ParticipantsN n@Int: #{n} Teilnehmer
TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen?
@ -1284,7 +1426,7 @@ TutorialDeleted: Tutorium gelöscht
TutorialRegisteredSuccess tutn@TutorialName: Erfolgreich zum Tutorium #{tutn} angemeldet
TutorialDeregisteredSuccess tutn@TutorialName: Erfolgreich vom Tutorium #{tutn} abgemeldet
TutorialNameTip: Muss eindeutig sein
TutorialNameTip: Muss innerhalb des Kurses eindeutig sein
TutorialCapacityNonPositive: Kapazität muss größer oder gleich null sein
TutorialCapacityTip: Beschränkt wieviele Studenten sich zu diesem Tutorium anmelden können
TutorialRegGroupTip: Studenten können sich in jeweils maximal einem Tutorium pro Registrierungs-Gruppe anmelden. Ist bei zwei oder mehr Tutorien keine Registrierungs-Gruppe gesetzt zählen diese als in verschiedenen Registrierungs-Gruppen
@ -1292,6 +1434,9 @@ TutorialRoomPlaceholder: Raum
TutorialTutors: Tutoren
TutorialTutorAlreadyAdded: Ein Tutor mit dieser E-Mail ist bereits für dieses Tutorium eingetragen
OccurrenceNoneScheduled: (Noch) keine planmäßigen Termine
OccurrenceNoneExceptions: (Noch) keine Termin-Ausnahmen
TutorialNew: Neues Tutorium
TutorialNameTaken tutn@TutorialName: Es existiert bereits anderes Tutorium mit Namen #{tutn}
@ -1395,7 +1540,7 @@ ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positi
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
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
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
ExamRoomManual': Keine automatische Zuteilung
@ -1441,7 +1586,7 @@ ExamPartName: Titel
ExamPartNameTip: Wird den Studierenden angezeigt
ExamPartMaxPoints: Maximalpunktzahl
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
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
@ -1511,6 +1656,8 @@ ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfun
ExamOfficeExamUsersHeading: Prüfungsleistungen
CsvFile: CSV-Datei
CsvImport: CSV-Import
CsvExport: CSV-Export
CsvModifyExisting: Existierende Einträge angleichen
CsvAddNew: Neue Einträge einfügen
CsvDeleteMissing: Fehlende Einträge entfernen
@ -1553,6 +1700,7 @@ CsvColumnExamUserCourseNote: Notizen zum Teilnehmer
CsvColumnUserName: Voller Name des Teilnehmers
CsvColumnUserMatriculation: Matrikelnummer des Teilnehmers
CsvColumnUserSex: Geschlecht
CsvColumnUserEmail: E-Mail Addresse des Teilnehmers
CsvColumnUserStudyFeatures: Alle aktiven Studiendaten des Teilnehmers als Semikolon (;) separierte Liste
CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
@ -1560,6 +1708,7 @@ CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach a
CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
CsvColumnUserRegistration: Zeitpunkt der Anmeldung zum Kurs (ISO 8601)
CsvColumnUserNote: Notizen zum Teilnehmer
CsvColumnUserTutorial: Tutorien zu denen der Teilnehmer angemeldet ist, als Semikolon (;) separierte Liste. Für Registrierungs-Gruppen unter den Tutorien gibt es jeweils eine weitere Spalte. Die Registrierungs-Gruppen-Spalten enthalten jeweils maximal ein Tutorium pro Teilnehmer. Sind alle Tutorien in Registrierungs-Gruppen, so gibt es keine Spalte "tutorial".
CsvColumnExamOfficeExamUserOccurrenceStart: Prüfungstermin (ISO 8601)
@ -1663,6 +1812,7 @@ SchoolFunctionInvitationAccepted school@SchoolName renderedFunction@Text: #{rend
AllocationActive: Aktiv
AllocationName: Name
AllocationAvailableCourses: Kurse
AllocationApplication: Bewerbung
AllocationAppliedCourses: Bewerbungen
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}
@ -1762,7 +1912,9 @@ SchoolExamOffice: Prüfungsamt
ApplicationEditTip: Während des Bewerbungszeitraums können eigene Bewerbungen beliebig angepasst und auch wieder zurückgezogen werden.
UserLdapSync: LDAP-Synchronisieren
AllUsersLdapSync: Alle LDAP-Synchronisieren
SynchroniseLdapUserQueued n@Int: LDAP-Synchronisation von #{n} #{pluralDE n "Benutzer" "Benutzern"} angestoßen
SynchroniseLdapAllUsersQueued: LDAP-Synchronisation von allen Benutzern angestoßen
UserHijack: Sitzung übernehmen
MailSubjectAllocationStaffRegister allocation@AllocationName: Sie können nun Kurse für die Zentralameldung „#{allocation}“ registrieren
@ -1813,6 +1965,7 @@ AdminUserDisplayEmail: Anzeige-E-Mail
AdminUserIdent: Identifikation
AdminUserAuth: Authentifizierung
AdminUserMatriculation: Matrikelnummer
AdminUserSex: Geschlecht
AuthKindLDAP: Campus-Kennung
AuthKindPWHash: Uni2work-Kennung
UserAdded: Benutzer erfolgreich angelegt
@ -1858,7 +2011,10 @@ AcceptApplicationsSecondaryRandom: Zufällig
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
CsvOptions: CSV-Optionen
CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt.
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
CsvTimestamp: Zeitstempel
CsvTimestampTip: Soll an den Namen jeder exportierten CSV-Datei ein Zeitstempel vorne angehängt werden?
CsvPresetRFC: Standard-Konform (RFC 4180)
CsvPresetExcel: Excel-Kompatibel
CsvCustom: Benutzerdefiniert
@ -1866,10 +2022,15 @@ CsvDelimiter: Trennzeichen
CsvUseCrLf: Zeilenumbrüche
CsvQuoting: Quoting
CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern?
CsvEncoding: Encoding
CsvEncodingTip: CSV-Dateien können in einer anderen Zeichenkodierung als dem vom System standardmäßig verwendeten UTF-8 exportiert werden. Beachten Sie dass es bei nicht-UTF-8 Zeichenkodierungen wmgl. zu Darstellungsproblemen mit Sonderzeichen kommt.
CsvUTF8: UTF-8 (Unicode)
CsvCP1252: Windows CP-1252 ("ANSI")
CsvDelimiterNull: Null-Byte
CsvDelimiterTab: Tabulator
CsvDelimiterComma: Komma
CsvDelimiterColon: Doppelpunkt
CsvDelimiterSemicolon: Strichpunkt
CsvDelimiterBar: Senkrechter Strich
CsvDelimiterSpace: Leerzeichen
CsvDelimiterUnitSep: Teilgruppentrennzeichen
@ -1912,11 +2073,17 @@ CourseDeregistrationAllocationShouldLog: Selbstverschuldet
CourseDeregistrationAllocationShouldLogTip: Falls der Platz des Studierenden, der abgemeldet wird, aus einer Zentralanmeldung stammt, ist vorgesehen einen permanenten Eintrag im System zu speichern, der den Studierenden u.U. bei zukünftigen Zentralanmeldungen benachteiligt. Als Kursverwalter haben Sie die Möglichkeit dies zu unterbinden, wenn der Studierende gute Gründe vorweisen kann, warum seine Abmeldung nicht selbstverschuldet ist.
MailSubjectAllocationResults allocation@AllocationName: Plätze für Zentralanmeldung „#{allocation}“ wurden verteilt
AllocationResultsLecturer: Es wurden Plätze zugewiesen, wie folgt:
AllocationResultLecturer csh@CourseShorthand count@Int64: #{count} Teilnehmer für #{csh}
AllocationResultsLecturer: Im Rahmen der oben genannten Zentralanmeldung wurden Plätze zugewiesen, wie folgt:
AllocationResultLecturer csh@CourseShorthand count@Int64 count2@Int64: #{count} Teilnehmer (von insgesamt #{count2}) für #{csh}
AllocationResultLecturerAll csh@CourseShorthand count@Int64: #{count} Teilnehmer für #{csh}
AllocationResultLecturerNone csh@CourseShorthand: Keine Teilnehmer für #{csh}
AllocationResultsStudent: Sie haben Plätze erhalten in:
AllocationNoResultsStudent: Sie haben leider keine Plätze erhalten.
AllocationResultStudent csh@CourseShorthand: Sie haben einen Platz in #{csh} erhalten.
AllocationResultsTip: Die folgenden Informationen entsprechen dem aktuellen Stand der Zentralanmeldung und können sich, z.B. durch die Verteilung von Plätzen an Nachrücker, noch ändern. Über zukünftige Änderungen, die Sie betreffen, werden Sie gesondert informiert.
AllocationResultsStudentTip: Unten aufgeführt sind alle Plätze, die Sie im Rahmen der genannten Zentralanmeldung erhalten haben und von denen Sie seit dem weder abgemeldet wurden, noch sich selbst abgemeldet haben. Plätze, über die Sie ggf. bereits informiert wurden, können also erneut aufgeführt sein.
AllocationResultStudentRegistrationTip: Sie sind zu oben genanntem Kurs in Uni2work angemeldet.
AllocationResultsStudentRegistrationTip: Sie sind zu den oben genannten Kursen in Uni2work angemeldet.
FavouriteVisited: Kürzlich besucht
FavouriteParticipant: Ihre Kurse
@ -1938,4 +2105,87 @@ CourseEventDeleteQuestion: Wollen Sie den unten aufgeführten Termin wirklich l
CourseEventDeleted: Kurstermin erfolgreich gelöscht
UserSimplifiedFeaturesOfStudyCsv: Vereinfachte Studiengangsdaten
UserSimplifiedFeaturesOfStudyCsvTip: Sollen Abschluss, Studiengang und Semester zur einfacheren Verarbeitung als separate Spalten exportiert werden? Es wird dann nur jeweils das Fach exportiert, dass der Student bei der Anmeldung ausgewählt hat.
UserSimplifiedFeaturesOfStudyCsvTip: Sollen Abschluss, Studiengang und Semester zur einfacheren Verarbeitung als separate Spalten exportiert werden? Es wird dann nur jeweils das Fach exportiert, dass der Student bei der Anmeldung ausgewählt hat.
Sex: Geschlecht
SexNotKnown: Unbekannt
SexMale: Männlich
SexFemale: Weiblich
SexNotApplicable: Keine Angabe
ShortSexNotKnown: unb.
ShortSexMale: m
ShortSexFemale: w
ShortSexNotApplicable: k.A.
ShowSex: Geschlechter anderer Nutzer anzeigen
ShowSexTip: Sollen in Kursteilnehmer-Tabellen u.Ä. die Geschlechter der Nutzer angezeigt werden?
StudySubTermsChildKey: Kind
StudySubTermsChildName: Kindname
StudySubTermsParentKey: Elter
StudySubTermsParentName: Eltername
StudyTermsDefaultDegree: Default Abschluss
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

2189
messages/uniworx/en-eu.msg Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,12 +21,15 @@ Allocation -- attributes with prefix staff- affect lecturers only, but are invis
registerByCourse UTCTime Maybe -- course registration dates are ignored until this day has passed or always prohibited
overrideDeregister UTCTime Maybe -- course deregistration enforced to be this date, i.e. students may disenrol from course after or never
-- overrideVisible not needed, since courses are always visible
fingerprint AllocationFingerprint Maybe
matchingLog FileId Maybe
TermSchoolAllocationShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolAllocationName term school name -- name must be unique within school and semester
deriving Show Eq Ord Generic
AllocationMatching
allocation AllocationId
fingerprint AllocationFingerprint
log FileId
AllocationCourse
allocation AllocationId
course CourseId

View File

@ -64,5 +64,5 @@ ExamCorrector
UniqueExamCorrector exam user
ExamPartCorrector
part ExamPartId
corrector ExamCorrector
corrector ExamCorrectorId
UniqueExamPartCorrector part corrector

View File

@ -6,3 +6,9 @@ File
content ByteString Maybe -- Nothing iff this is a directory
modified UTCTime
deriving Show Eq Generic
SessionFile
user UserId
reference SessionFileReference
file FileId
touched UTCTime

View File

@ -16,3 +16,10 @@ CronLastExec
time UTCTime -- When was the job executed
instance InstanceId -- Which uni2work-instance did the work
UniqueCronLastExec job
SentNotification
content Value
user UserId
time UTCTime
instance InstanceId

View File

@ -6,8 +6,8 @@ Sheet -- exercise sheet for a given course
grouping SheetGroup -- May participants submit in groups of certain sizes?
markingText Html Maybe -- Instructons for correctors, included in marking templates
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
activeFrom UTCTime -- Download of questions and submission is permitted afterwards
activeTo UTCTime -- Submission is only permitted before
activeFrom UTCTime Maybe -- Download of questions and submission is permitted afterwards
activeTo UTCTime Maybe -- Submission is only permitted before
hintFrom UTCTime Maybe -- Additional files are made available
solutionFrom UTCTime Maybe -- Solution is made available
submissionMode SubmissionMode -- Submission upload by students and/or through tutors?

View File

@ -3,13 +3,14 @@ Tutorial json
course CourseId
type (CI Text) -- "Tutorium", "Zentralübung", ...
capacity Int Maybe -- limit for enrolment in this tutorial
room Text
room Text Maybe
time Occurrences
regGroup (CI Text) Maybe -- each participant may register for one tutorial per regGroup
registerFrom UTCTime Maybe
registerTo UTCTime Maybe
deregisterUntil UTCTime Maybe
lastChanged UTCTime default=now()
tutorControlled Bool default=false
UniqueTutorial course name
deriving Generic
Tutor

View File

@ -28,10 +28,12 @@ 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
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)
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
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
csvOptions CsvOptions "default='{}'::jsonb"
sex Sex Maybe
showSex Bool default=false
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
UniqueEmail email -- Column 'email' can be used as a row-key in this table
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
@ -53,11 +55,13 @@ StudyFeatures -- multiple entries possible for students pursuing several degree
user UserId
degree StudyDegreeId -- Abschluss, i.e. Master, Bachelor, etc.
field StudyTermsId -- Fach, i.e. Informatics, Philosophy, etc.
superField StudyTermsId Maybe
type StudyFieldType -- Major or minor, i.e. Haupt-/Nebenfach
semester Int
updated UTCTime default=now() -- last update from LDAP
valid Bool default=true -- marked as active in LDAP (students may switch, but LDAP never forgets)
UniqueStudyFeatures user degree field type semester
deriving Eq Show
-- UniqueUserSubject ubuser degree field -- There exists a counterexample
StudyDegree -- Studienabschluss
key Int -- LMU-internal key
@ -65,19 +69,43 @@ StudyDegree -- Studienabschluss
name Text Maybe -- description given by LDAP
Primary key -- column key is used as actual DB row key
-- newtype Key StudyDegree = StudyDegreeKey' { unStudyDegreeKey :: Int }
deriving Show
deriving Eq Show
StudyTerms -- Studiengang
key Int -- LMU-internal key
key Int -- standardised key
shorthand Text Maybe -- admin determined shorthand
name Text Maybe -- description given by LDAP
defaultDegree StudyDegreeId Maybe
defaultType StudyFieldType Maybe
Primary key -- column key is used as actual DB row key
-- newtype Key StudyTerms = StudyTermsKey' { unStudyTermsKey :: Int }
deriving Show
StudyTermCandidate -- No one at LMU is willing and able to tell us the meaning of the keys for StudyDegrees and StudyTerms.
deriving Eq Ord Show
StudySubTerms
child StudyTermsId
parent StudyTermsId
UniqueStudySubTerms child parent
StudyTermNameCandidate -- No one at LMU is willing and able to tell us the meaning of the keys for StudyDegrees and StudyTerms.
-- Each LDAP login provides an unordered set of keys and an unordered set of plain text description with an unknown 1-1 correspondence.
-- This table helps us to infer which key belongs to which plain text by recording possible combinations at login.
-- If a login provides n keys and n plan texts, then n^2 rows with the same incidence are created, storing all combinations
incidence TermCandidateIncidence -- random id, generated once per login to associate matching pairs
key Int -- a possible key for the studyTermName
key Int -- a possible key for the studyTermName or studySubTermName
name Text -- studyTermName as plain text from LDAP
deriving Show Eq Ord
StudySubTermParentCandidate
incidence TermCandidateIncidence
key Int
parent Int
deriving Show Eq Ord
StudyTermStandaloneCandidate
incidence TermCandidateIncidence
key Int
deriving Show Eq Ord
UserGroupMember
group UserGroupName
user UserId
primary Checkmark nullable
UniquePrimaryUserGroupMember group primary !force
UniqueUserGroupMember group user

6331
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{
"name": "uni2work",
"version": "7.13.0",
"version": "10.1.0",
"description": "",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"start": "run-p frontend:build:watch yesod:start",
"start": "npm-run-all frontend:build --parallel \"frontend:build:watch\" \"yesod:start\"",
"test": "run-s frontend:test yesod:test",
"lint": "run-s frontend:lint yesod:lint",
"build": "run-s frontend:build yesod:build",
@ -20,8 +20,8 @@
"frontend:lint": "eslint frontend/src",
"frontend:test": "karma start --conf karma.conf.js",
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false",
"frontend:build": "webpack",
"frontend:build:watch": "webpack --watch",
"frontend:build": "webpack --progress",
"frontend:build:watch": "webpack --watch --progress",
"prerelease": "./is-clean.sh && npm run test",
"release": "standard-version -a",
"postrelease": "git push --follow-tags origin master"
@ -42,36 +42,40 @@
"scripts": {
"postbump": "./sync-versions.hs && git add -- package.yaml"
},
"commitUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/{{hash}}",
"compareUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/{{id}}",
"userUrlFormat": "https://gitlab.cip.ifi.lmu.de/{{user}}"
"commitUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/{{hash}}",
"compareUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/{{id}}",
"userUrlFormat": "https://gitlab2.rz.ifi.lmu.de/{{user}}"
},
"browserslist": [
"defaults"
],
"devDependencies": {
"@babel/cli": "^7.6.3",
"@babel/core": "^7.6.3",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.6.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.6.3",
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@commitlint/cli": "^8.2.0",
"@commitlint/config-conventional": "^8.2.0",
"autoprefixer": "^9.6.4",
"@fortawesome/fontawesome-pro": "^5.12.0",
"autoprefixer": "^9.7.3",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-preset-es2015": "^6.24.1",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.0",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^5.0.2",
"husky": "^2.7.0",
"jasmine-core": "^3.5.0",
"karma": "^4.3.0",
"js-yaml": "^3.13.1",
"karma": "^4.4.1",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^2.0.0",
"karma-jasmine": "^2.0.1",
@ -80,23 +84,33 @@
"karma-webpack": "^3.0.5",
"lint-staged": "^8.2.1",
"lodash.debounce": "^4.0.8",
"node-sass": "^4.12.0",
"mini-css-extract-plugin": "^0.8.0",
"npm-run-all": "^4.1.5",
"null-loader": "^2.0.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0",
"resolve-url-loader": "^3.1.1",
"sass": "^1.23.7",
"sass-loader": "^7.3.1",
"semver": "^6.3.0",
"standard-version": "^6.0.1",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.41.0",
"webpack-cli": "^3.3.9"
"terser-webpack-plugin": "^2.2.3",
"typeface-roboto": "0.0.75",
"typeface-source-sans-pro": "0.0.75",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-manifest-plugin": "^2.2.0",
"webpack-plugin-hash-output": "^3.2.1"
},
"dependencies": {
"@babel/runtime": "^7.6.3",
"core-js": "^3.2.1",
"@babel/runtime": "^7.7.6",
"@juggle/resize-observer": "^2.5.0",
"core-js": "^3.4.8",
"moment": "^2.24.0",
"npm": "^6.12.0",
"tail.datetime": "git+https://github.com/uni2work/tail.DateTime.git#master",
"npm": "^6.13.4",
"tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#master",
"whatwg-fetch": "^3.0.0"
}
}

View File

@ -1,41 +1,37 @@
name: uniworx
version: 7.13.0
version: 10.1.0
dependencies:
- base >=4.9.1.0 && <5
- yesod >=1.6 && <1.7
- yesod-core >=1.6 && <1.7
- yesod-auth >=1.6 && <1.7
- yesod-static >=1.6 && <1.7
- yesod-form >=1.6 && <1.7
- classy-prelude >=1.5 && <1.6
- classy-prelude-conduit >=1.5 && <1.6
- classy-prelude-yesod >=1.5 && <1.6
- bytestring >=0.10 && <0.11
- text >=0.11 && <2.0
- persistent >=2.9 && <2.10
- persistent-postgresql >=2.9 && <2.10
- persistent-template >=2.5 && <2.9
- persistent-qq >=2.9 && <2.10
- base
- yesod
- yesod-core
- yesod-auth
- yesod-static
- yesod-form
- classy-prelude
- classy-prelude-yesod
- bytestring
- text
- persistent
- persistent-postgresql
- persistent-template
- persistent-qq
- template-haskell
- shakespeare >=2.0 && <2.1
- hjsmin >=0.1 && <0.3
- monad-control >=0.3 && <1.1
- wai-extra >=3.0 && <3.1
- yaml >=0.11 && <0.12
- http-conduit >=2.3 && <2.4
- directory >=1.1 && <1.4
- warp >=3.0 && <3.3
- shakespeare
- monad-control
- wai-extra
- yaml
- http-conduit
- directory
- warp
- data-default
- aeson >=1.4 && <1.5
- conduit >=1.0 && <2.0
- conduit-combinators
- monad-logger >=0.3 && <0.4
- fast-logger >=2.2 && <2.5
- wai-logger >=2.2 && <2.4
- aeson
- conduit
- monad-logger
- fast-logger
- wai-logger
- foreign-store
- file-embed
- safe
- unordered-containers
- containers
- vector
@ -67,8 +63,7 @@ dependencies:
- binary
- cereal
- mtl
- sandi
- esqueleto
- esqueleto >=3.1.0
- mime-types
- generic-deriving
- blaze-html
@ -78,8 +73,6 @@ dependencies:
- uuid
- exceptions
- stm
- stm-chans
- stm-conduit
- lens
- MonadRandom
- email-validate
@ -140,6 +133,10 @@ dependencies:
- retry
- generic-lens
- array
- cookie
- prometheus-client
- prometheus-metrics-ghc
- wai-middleware-prometheus
other-extensions:
- GeneralizedNewtypeDeriving
@ -233,10 +230,7 @@ executables:
uniworx:
main: main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -T"
dependencies:
- uniworx
when:

743
records.json Normal file
View File

@ -0,0 +1,743 @@
{
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/app.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/alerts/alerts.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-form/async-form.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/asidenav/asidenav.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table-filter.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/async-table/async-table.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/show-hide/show-hide.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/form/form.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/inputs.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/radio.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/mass-input/mass-input.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/hide-columns/hide-columns.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/navbar/navbar.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/course-teaser/course-teaser.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/tooltips/tooltips.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/modal/modal.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/file-input.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/sass-loader/dist/cjs.js??ref--6-3!frontend/src/utils/inputs/checkbox.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!frontend/src/utils/form/datepicker.css": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"modules": {
"byIdentifier": {
"multi frontend/src/polyfill.js frontend/src/main.js": 0
},
"usedIds": {
"0": 0
}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
},
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/app.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/alerts/alerts.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/asidenav/asidenav.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/show-hide/show-hide.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-form/async-form.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-table/async-table-filter.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-table/async-table.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/form/form.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/inputs.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/radio.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/mass-input/mass-input.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/hide-columns/hide-columns.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/navbar/navbar.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/tooltips/tooltips.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/course-teaser/course-teaser.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/modal/modal.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/file-input.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/checkbox.scss": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--5-1!node_modules/postcss-loader/src/index.js??ref--5-2!node_modules/resolve-url-loader/index.js??ref--5-3!frontend/src/utils/form/datepicker.css": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/app.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-form/async-form.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/alerts/alerts.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/asidenav/asidenav.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/form/form.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/hide-columns/hide-columns.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/navbar/navbar.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/course-teaser/course-teaser.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/modal/modal.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/tooltips/tooltips.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/mass-input/mass-input.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/radio.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/inputs.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/show-hide/show-hide.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-table/async-table.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/async-table/async-table-filter.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/file-input.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"mini-css-extract-plugin node_modules/css-loader/dist/cjs.js??ref--6-1!node_modules/postcss-loader/src/index.js??ref--6-2!node_modules/resolve-url-loader/index.js??ref--6-3!node_modules/sass-loader/dist/cjs.js??ref--6-4!frontend/src/utils/inputs/checkbox.sass": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
]
}

15
routes
View File

@ -40,7 +40,8 @@
/auth AuthR Auth getAuth !free
/favicon.ico FaviconR GET !free
/robots.txt RobotsR GET !free
/robots.txt RobotsR GET !free
/metrics MetricsR GET
/ HomeR GET !free
/users UsersR GET POST -- no tags, i.e. admins only
@ -61,9 +62,9 @@
/instance InstanceR GET !free
/info InfoR GET !free
/info/lecturer InfoLecturerR GET !lecturer
/info/data DataProtR GET !free
/info/legal LegalR GET !free
/info/allocation InfoAllocationR GET !free
/impressum ImpressumR GET !free
/info/glossary GlossaryR GET !free
/version VersionR GET !free
/help HelpR GET POST !free
@ -73,6 +74,7 @@
/user/authpreds AuthPredsR GET POST !free
/user/set-display-email SetDisplayEmailR GET POST !free
/user/csv-options CsvOptionsR GET POST !free
/user/lang LangR POST !free
/exam-office ExamOfficeR !exam-office:
/ EOExamsR GET
@ -113,7 +115,7 @@
/users CUsersR GET POST
!/users/new CAddUserR GET POST !lecturerANDallocation-time
!/users/invite CInviteR GET POST
/users/#CryptoUUIDUser CUserR GET POST !lecturerANDparticipant
/users/#CryptoUUIDUser CUserR GET POST !lecturerANDparticipant !lecturerANDapplicant
/correctors CHiWisR GET
/communication CCommR GET POST
/notes CNotesR GET POST !corrector -- THIS route is used to check for overall course corrector access!
@ -141,7 +143,6 @@
/invite SInviteR GET POST !ownerANDtimeANDuser-submissions
!/#SubmissionFileType SubArchiveR 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
/pseudonym SPseudonymR GET POST !course-registeredANDcorrector-submissions
/corrector-invite/ SCorrInviteR GET POST
@ -158,12 +159,12 @@
/tuts CTutorialListR GET !tutor -- THIS route is used to check for overall course tutor access!
/tuts/new CTutorialNewR GET POST
/tuts/#TutorialName TutorialR:
/edit TEditR GET POST
/edit TEditR GET POST !tutorANDtutor-control
/delete TDeleteR GET POST
/participants TUsersR GET POST !tutor
/register TRegisterR POST !timeANDcapacityANDcourse-registeredANDregister-group !timeANDtutorial-registered
/communication TCommR GET POST !tutor
/tutor-invite TInviteR GET POST
/tutor-invite TInviteR GET POST !tutorANDtutor-control
/exams CExamListR GET !free
/exams/new CExamNewR GET POST
/exams/#ExamName ExamR:

View File

@ -19,10 +19,12 @@ let
'';
override = oldAttrs: {
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install ]);
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap google-chrome ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install weeder ]);
shellHook = ''
export PROMPT_INFO="${oldAttrs.name}"
export CHROME_BIN=$(which google-chrome-stable)
if [[ -z "$PGHOST" ]]; then
set -xe

View File

@ -22,6 +22,8 @@ import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr,
import Import hiding (cancel)
import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware)
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as HTTP
import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException,
runSettings, runSettingsSocket, setHost,
@ -48,6 +50,7 @@ import System.Directory
import Jobs
import qualified Data.Text.Encoding as Text
import Yesod.Auth.Util.PasswordStore
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 Control.Concurrent.STM.Delay
import Control.Monad.STM (retry)
import Control.Monad.Trans.Cont (runContT, callCC)
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.
-- (HPack takes care to add new modules to our cabal file nowadays.)
@ -111,6 +122,7 @@ import Handler.Health
import Handler.Exam
import Handler.Allocation
import Handler.ExamOffice
import Handler.Metrics
-- This line actually creates our YesodDispatch instance. It is the second half
@ -124,6 +136,8 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- migrations handled by Yesod.
makeFoundation :: (MonadResource m, MonadUnliftIO m) => AppSettings -> m UniWorX
makeFoundation appSettings'@AppSettings{..} = do
void $ Prometheus.register Prometheus.ghcMetrics
-- Some basic initializations: HTTP connection manager, logger, and static
-- subsite.
appHttpManager <- newManager
@ -290,7 +304,39 @@ makeApplication foundation = liftIO $ do
logWare <- makeLogWare foundation
-- Create the WAI application and apply middlewares
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 app = do
@ -320,13 +366,22 @@ makeLogWare app = do
logWare <- either mkLogWare return lookupRes
logWare wai req fin
data ReadySince = MkReadySince
-- | Warp settings for the given foundation value.
warpSettings :: UniWorX -> Settings
warpSettings foundation = defaultSettings
& setBeforeMainLoop (runAppLoggingT foundation $ do
let notifyReady = do
$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
| foundation ^. _appHealthCheckDelayNotify
-> void . forkIO $ do
@ -425,39 +480,46 @@ appMain = runResourceT $ do
case watchdogMicroSec of
Just wInterval
| maybe True (== myProcessID) watchdogProcess
-> let notifyWatchdog :: IO ()
-> let notifyWatchdog :: forall a. IO a
notifyWatchdog = runAppLoggingT foundation $ go Nothing
where
go pStatus = do
d <- liftIO . newDelay . floor $ wInterval % 2
go :: Maybe (Set (UTCTime, HealthReport)) -> LoggingT IO a
go pResults = do
let delay = floor $ wInterval % 2
d <- liftIO $ newDelay delay
status <- atomically $ asum
[ Nothing <$ waitDelay d
, Just <$> do
$logDebugS "Notify" $ "Waiting up to " <> tshow delay <> "µs..."
mResults <- atomically $ asum
[ pResults <$ waitDelay d
, do
results <- readTVar $ foundation ^. _appHealthReport
case fromNullable results of
Nothing -> retry
Just rs -> do
let status = ofoldMap1 (Max *** Min . healthReportStatus) rs
guard $ pStatus /= Just status
return status
guardOn (pResults /= Just results) $ Just results
]
case status of
Just (_, Min status') -> do
$logInfoS "NotifyStatus" $ toPathPiece status'
liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status'
Nothing -> return ()
$logDebugS "Notify" "Checking for status/watchdog..."
(*> go mResults) . void . runMaybeT $ do
results <- hoistMaybe mResults
case status of
Just (_, Min HealthSuccess) -> do
$logInfoS "NotifyWatchdog" "Notify"
liftIO $ void Systemd.notifyWatchdog
_other -> return ()
Min status <- hoistMaybe $ ofoldMap1 (Min . healthReportStatus . view _2) <$> fromNullable results
$logInfoS "NotifyStatus" $ toPathPiece status
liftIO . void . Systemd.notifyStatus . unpack $ toPathPiece status
go status
in void $ allocateLinkedAsync notifyWatchdog
_other -> return ()
now <- liftIO getCurrentTime
iforM_ (foundation ^. _appHealthCheckInterval) . curry $ \case
(_, 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
case sockets of

View File

@ -132,6 +132,12 @@ data Transaction
{ transactionOffice :: UserId
, transactionField :: StudyTermsId
}
| TransactionTutorialEdit
{ transactionTutorial :: TutorialId
}
| TransactionTutorialDelete
{ transactionTutorial :: TutorialId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)

View File

@ -13,6 +13,7 @@ import qualified Data.CaseInsensitive as CI
data DummyMessage = MsgDummyIdent
| MsgDummyIdentPlaceholder
| MsgDummyNoFormData
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
@ -24,7 +25,9 @@ dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
, Button (HandlerSite m) ButtonSubmit
, MonadHandler m
) => 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
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)

View File

@ -7,7 +7,7 @@ module Auth.LDAP
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname
, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName
, ldapUserSchoolAssociation
, ldapUserSchoolAssociation, ldapUserSubTermsSemester, ldapSex
) where
import Import.NoFoundation
@ -36,6 +36,7 @@ data CampusLogin = CampusLogin
data CampusMessage = MsgCampusIdentPlaceholder
| MsgCampusIdent
| MsgCampusPassword
| MsgCampusPasswordPlaceholder
| MsgCampusSubmit
| MsgCampusInvalidCredentials
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
@ -47,10 +48,12 @@ findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not
userFilters =
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
, ldapUserPrincipalName Ldap.:= Text.encodeUtf8 [st|#{ident}@campus.lmu.de|]
, ldapUserEmail Ldap.:= Text.encodeUtf8 ident
, ldapUserEmail Ldap.:= Text.encodeUtf8 [st|#{ident}@lmu.de|]
, ldapUserEmail Ldap.:= Text.encodeUtf8 [st|#{ident}@campus.lmu.de|]
, ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident
] ++
[ ldapUserEmail' Ldap.:= Text.encodeUtf8 ident'
| ident' <- [ident, [st|#{ident}@lmu.de|], [st|#{ident}@campus.lmu.de|]]
, ldapUserEmail' <- toList ldapUserEmail
] ++
[ ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident
]
userSearchSettings = mconcat
[ Ldap.scope ldapScope
@ -59,17 +62,23 @@ findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not
, Ldap.derefAliases Ldap.DerefAlways
]
ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation :: Ldap.Attr
ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
ldapUserEmail = Ldap.Attr "mail"
ldapUserDisplayName = Ldap.Attr "displayName"
ldapUserMatriculation = Ldap.Attr "LMU-Stud-Matrikelnummer"
ldapUserFirstName = Ldap.Attr "givenName"
ldapUserSurname = Ldap.Attr "sn"
ldapUserTitle = Ldap.Attr "title"
ldapUserStudyFeatures = Ldap.Attr "dfnEduPersonFeaturesOfStudy"
ldapUserFieldName = Ldap.Attr "dfnEduPersonFieldOfStudyString"
ldapUserFieldName = Ldap.Attr "LMU-Stg-Fach"
ldapUserSchoolAssociation = Ldap.Attr "LMU-IFI-eduPersonOrgUnitDNString"
ldapSex = Ldap.Attr "schacGender"
ldapUserSubTermsSemester = Ldap.Attr "LMU-Stg-FachundFS"
ldapUserEmail :: NonEmpty Ldap.Attr
ldapUserEmail = Ldap.Attr "mail" :|
[ Ldap.Attr "name"
]
data CampusUserException = CampusUserLdapError LdapPoolError
@ -122,7 +131,7 @@ campusForm = do
MsgRenderer mr <- getMsgRenderer
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
<$> ident

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