Merge branch 'master' of gitlab2.rz.ifi.lmu.de:uni2work/uni2work

This commit is contained in:
Sarah Vaupel 2020-01-14 11:11:35 +01:00
commit e4393972f8
61 changed files with 1837 additions and 305 deletions

4
.gitignore vendored
View File

@ -35,5 +35,7 @@ test.log
/.stack-work.lock /.stack-work.lock
/.npmrc /.npmrc
/config/webpack.yml /config/webpack.yml
static/wp-*/
tunnel.log tunnel.log
/static
/well-known
/**/tmp-*

View File

@ -33,10 +33,13 @@ npm install:
- n stable - n stable
- npm install -g npm - npm install -g npm
- hash -r - hash -r
- apt-get install openssh-client -y - apt-get -y install openssh-client exiftool
- install -v -m 0700 -d ~/.ssh - install -v -m 0700 -d ~/.ssh
- install -v -T -m 0644 ${SSH_KNOWN_HOSTS} ~/.ssh/known_hosts - 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; - install -v -T -m 0400 ${SSH_DEPLOY_KEY} ~/.ssh/deploy && echo "IdentityFile ~/.ssh/deploy" >> ~/.ssh/config;
after_script:
- zip -qr node_modules.zip node_modules
- du -hs node_modules node_modules.zip
artifacts: artifacts:
paths: paths:
- node_modules/ - node_modules/
@ -54,6 +57,7 @@ frontend:build:
artifacts: artifacts:
paths: paths:
- static - static
- well-known
- config/webpack.yml - config/webpack.yml
name: "${CI_JOB_NAME}" name: "${CI_JOB_NAME}"
expire_in: "1 day" expire_in: "1 day"

View File

@ -2,6 +2,45 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [10.3.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.2.0...v10.3.0) (2020-01-12)
### Bug Fixes
* fix app frontend test ([49bafe1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/49bafe1))
* improve exam occurrence ui ([83fa9c9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/83fa9c9))
* improve labeling of button to switch exam occurrence ([727b89b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/727b89b))
* tweak debouncing & canceling ([6b51cc5](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6b51cc5))
* **async-table:** bind callback in updateTableFrom call ([cd3e72c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/cd3e72c))
* **util-registry:** fix initAll and tests ([2620fb2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2620fb2))
* **util-registry:** start setup instances and not all active instances ([ddf94bf](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ddf94bf))
### Features
* support exam registration including room (ExamRoomFifo) ([14bb020](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/14bb020))
* well known files ([068632b](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/068632b))
* **async-table:** no submit on locked inputs ([22b3780](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/22b3780))
* **frontend:** split up util registry ([67e472f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/67e472f))
* **util-registry:** more debug info for setup util instances ([00584f9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/00584f9))
## [10.2.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.1.0...v10.2.0) (2020-01-07)
### Bug Fixes
* divide by zero ([674b949](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/674b949))
### Features
* generate & include new favicon ([b78c484](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b78c484))
* **config:** improve configurability of VerpMode ([a7c3fe7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a7c3fe7))
## [10.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.0.1...v10.1.0) (2019-12-23) ## [10.1.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.0.1...v10.1.0) (2019-12-23)

29
assets/favicon.svg Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
version="1.1"
viewBox="0 0 576 512">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="stroke-width:0.89999998;fill:#0a9342;fill-opacity:1"
id="path2"
d="m 516.7975,54.444679 c -49.32,2.799 -147.348,12.987 -207.864,50.031001 -4.176,2.556 -6.543,7.101 -6.543,11.853 v 327.483 c 0,10.395 11.367,16.965 20.952,12.141 62.262,-31.338 152.307,-39.888 196.83,-42.228 15.201,-0.801 27.018,-12.987 27.018,-27.594 V 82.074679 c 0.009,-15.939 -13.815,-28.566 -30.393,-27.63 z M 267.0565,104.47568 C 206.5495,67.431679 108.5215,57.252679 59.2015,54.444679 c -16.578,-0.936 -30.402,11.691 -30.402,27.63 V 386.13968 c 0,14.616 11.817,26.802 27.018,27.594 44.541,2.34 134.631,10.899 196.893,42.255 9.558,4.815 20.889,-1.746 20.889,-12.114 v -327.708 c 0,-4.761 -2.358,-9.126 -6.543,-11.691 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

77
config/favicon.json Normal file
View File

@ -0,0 +1,77 @@
{
"masterPicture": "assets/favicon.svg",
"design": {
"desktop_browser": {},
"ios": {
"picture_aspect": "background_and_margin",
"margin": "5%",
"background_color": "#ffffff",
"startup_image": {
"background_color": "#ffffff"
},
"app_name": "Uni2work",
"assets": {
"ios6_and_prior_icons": false,
"ios7_and_later_icons": true,
"precomposed_icons": true,
"declare_only_default_icon": true
}
},
"windows": {
"picture_aspect": "white_silhouette",
"background_color": "#0a9342",
"app_name": "Uni2work"
},
"firefox_app": {
"picture_aspect": "circle",
"keep_picture_in_circle": false,
"circle_inner_margin": "5%",
"background_color": "#ffffff",
"overlay": false,
"manifest": {
"app_name": "Uni2work",
"app_description": {
"_i18n": true,
"de-de-formal": "Ein webbasiertes Lehrverwaltungssystem der LMU München",
"en-eu": "A web based teaching management system at LMU Munich"
},
"developer_name": "Uni2work-Team",
"developer_url": "https://uni2work.ifi.lmu.de/info",
"display": "browser",
"start_url": "/"
}
},
"android_chrome": {
"picture_aspect": "shadow",
"manifest": {
"name": "Uni2work",
"display": "browser",
"orientation": "portrait",
"start_url": "/"
},
"assets": {
"legacy_icon": true,
"low_resolution_icons": false
}
},
"safari_pinned_tab": {
"picture_aspect": "silhouette",
"theme_color": "#0a9342"
},
"coast": {
"picture_aspect": "background_and_margin",
"background_color": "#ffffff",
"margin": "10%"
},
"open_graph": {
"picture_aspect": "background_and_margin",
"background_color": "#ffffff",
"margin": "10%",
"ratio": "square"
}
},
"settings": {
"html_code_file": true
},
"versioning": false
}

View File

@ -43,6 +43,8 @@ application/java-vm class
application/javascript js application/javascript js
application/json json application/json json
application/jsonml+json jsonml application/jsonml+json jsonml
application/manifest+json webmanifest
application/x-web-app-manifest+json webapp
application/lost+xml lostxml application/lost+xml lostxml
application/mac-binhex40 hqx application/mac-binhex40 hqx
application/mac-compactpro cpt application/mac-compactpro cpt

View File

@ -4,6 +4,9 @@
# See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings # See https://github.com/yesodweb/yesod/wiki/Configuration#parsing-numeric-values-as-strings
static-dir: "_env:STATIC_DIR:static" static-dir: "_env:STATIC_DIR:static"
well-known-dir: "_env:WELL_KNOWN_DIR:well-known"
well-known-link-file: "html_code.html"
webpack-manifest: "_env:WEBPACK_MANIFEST:config/webpack.yml" webpack-manifest: "_env:WEBPACK_MANIFEST:config/webpack.yml"
host: "_env:HOST:*4" # any IPv4 host host: "_env:HOST:*4" # any IPv4 host
port: "_env:PORT:3000" port: "_env:PORT:3000"
@ -14,8 +17,8 @@ mail-from:
email: "_env:MAILFROM_EMAIL:uniworx@localhost" email: "_env:MAILFROM_EMAIL:uniworx@localhost"
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost" mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
mail-verp: mail-verp:
separator: "+" separator: "_env:VERP_SEPARATOR:+"
at-replacement: "=" at-replacement: "_env:VERP_AT_REPLACEMENT:="
mail-support: mail-support:
name: "_env:MAILSUPPORT_NAME:" name: "_env:MAILSUPPORT_NAME:"
email: "_env:MAILSUPPORT:uni2work@ifi.lmu.de" email: "_env:MAILSUPPORT:uni2work@ifi.lmu.de"

View File

@ -15,7 +15,7 @@ export class App {
constructor() { constructor() {
this.utilRegistry.setApp(this); this.utilRegistry.setApp(this);
document.addEventListener('DOMContentLoaded', () => this.utilRegistry.setupAll()); document.addEventListener('DOMContentLoaded', () => this.utilRegistry.initAll());
} }
registerUtilities(utils) { registerUtilities(utils) {

View File

@ -23,10 +23,10 @@ describe('App', () => {
expect(global.App).toBeTruthy(); expect(global.App).toBeTruthy();
}); });
it('should setup all utlites when page is done loading', () => { it('should init all utlites when page is done loading', () => {
spyOn(global.App.utilRegistry, 'setupAll'); spyOn(global.App.utilRegistry, 'initAll');
document.dispatchEvent(new Event('DOMContentLoaded')); document.dispatchEvent(new Event('DOMContentLoaded'));
expect(global.App.utilRegistry.setupAll).toHaveBeenCalled(); expect(global.App.utilRegistry.initAll).toHaveBeenCalled();
}); });
describe('provides services', () => { describe('provides services', () => {

View File

@ -14,11 +14,16 @@ export class UtilRegistry {
* selector: string | utils selector, e.g. '[uw-example]' * selector: string | utils selector, e.g. '[uw-example]'
* setup: Function | utils setup function, see below * setup: Function | utils setup function, see below
* *
* optional util properties:
* start: Function | utils start function, see below
*
* setup function must return instance object with at least these properties: * setup function must return instance object with at least these properties:
* name: string | utils name * name: string | utils name
* element: HTMLElement | element the util is applied to * element: HTMLElement | element the util is applied to
* destroy: Function | function to destroy the util and remove any listeners * destroy: Function | function to destroy the util and remove any listeners
* *
* (optional) start function for registering event listeners
*
* @param util Object Utility that should be added to the registry * @param util Object Utility that should be added to the registry
*/ */
register(util) { register(util) {
@ -45,13 +50,26 @@ export class UtilRegistry {
this._appInstance = appInstance; this._appInstance = appInstance;
} }
setupAll(scope) { initAll(scope) {
let startedInstances = [];
const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat();
setupInstances.forEach((utilInstance) => {
if (utilInstance) {
const instance = utilInstance.instance;
if (instance && typeof instance.start === 'function') {
instance.start();
startedInstances.push(instance);
}
}
});
if (DEBUG_MODE > 1) { if (DEBUG_MODE > 1) {
console.info('registered js utilities:'); console.info('initialized js util instances:');
console.table(this._registeredUtils); console.table(setupInstances);
} }
this._registeredUtils.forEach((util) => this.setup(util, scope)); return startedInstances;
} }
setup(util, scope = document.body) { setup(util, scope = document.body) {
@ -71,7 +89,7 @@ export class UtilRegistry {
utilInstance = new util(element, this._appInstance); utilInstance = new util(element, this._appInstance);
} catch(err) { } catch(err) {
if (DEBUG_MODE > 0) { if (DEBUG_MODE > 0) {
console.warn('Error while trying to initialize a utility!', { util , element, err }); console.error('Error while trying to initialize a utility!', { util , element, err });
} }
} }
@ -80,7 +98,7 @@ export class UtilRegistry {
console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); console.info('Got utility instance for utility "' + util.name + '"', { utilInstance });
} }
instances.push(utilInstance); instances.push({ util: util, scope: scope, element: element, instance: utilInstance });
} }
}); });
} }

View File

@ -97,32 +97,80 @@ describe('UtilRegistry', () => {
const setupUtilities = utilRegistry.setup(TestUtil1, testScope); const setupUtilities = utilRegistry.setup(TestUtil1, testScope);
expect(setupUtilities).toBeTruthy(); expect(setupUtilities).toBeTruthy();
expect(setupUtilities[0].app).toBe(fakeApp); setupUtilities.forEach((setupUtility) => {
expect(setupUtilities[1].app).toBe(fakeApp); expect(setupUtility).toBeTruthy();
expect(setupUtility.instance).toBeTruthy();
});
expect(setupUtilities[0].instance.app).toBe(fakeApp);
expect(setupUtilities[1].instance.app).toBe(fakeApp);
}); });
}); });
}); });
describe('setupAll()', () => { describe('initAll()', () => {
it('should setup all the utilities', () => { it('should setup all the utilities', () => {
spyOn(utilRegistry, 'setup'); spyOn(utilRegistry, 'setup');
utilRegistry.register(TestUtil1); utilRegistry.register(TestUtil1);
utilRegistry.register(TestUtil2); utilRegistry.register(TestUtil2);
utilRegistry.setupAll(); utilRegistry.register(TestUtil3);
utilRegistry.initAll();
expect(utilRegistry.setup.calls.count()).toBe(2); expect(utilRegistry.setup.calls.count()).toBe(3);
expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TestUtil1, undefined]); expect(utilRegistry.setup.calls.argsFor(0)).toEqual([TestUtil1, undefined]);
expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TestUtil2, undefined]); expect(utilRegistry.setup.calls.argsFor(1)).toEqual([TestUtil2, undefined]);
expect(utilRegistry.setup.calls.argsFor(2)).toEqual([TestUtil3, undefined]);
}); });
it('should pass the given scope', () => { it('should pass the given scope', () => {
spyOn(utilRegistry, 'setup'); spyOn(utilRegistry, 'setup');
utilRegistry.register(TestUtil1); utilRegistry.register(TestUtil1);
const scope = document.createElement('div'); const scope = document.createElement('div');
utilRegistry.setupAll(scope); utilRegistry.initAll(scope);
expect(utilRegistry.setup).toHaveBeenCalledWith(TestUtil1, scope); expect(utilRegistry.setup).toHaveBeenCalledWith(TestUtil1, scope);
}); });
describe('starts startable util instances', () => {
let testScope,
testElement1,
testElement2,
testElement3,
testElement4;
beforeEach(() => {
testScope = document.createElement('div');
testElement1 = document.createElement('div');
testElement2 = document.createElement('div');
testElement3 = document.createElement('div');
testElement4 = document.createElement('div');
testElement1.classList.add('util1');
testElement2.id = 'util2';
testElement3.classList.add('util3');
testElement4.classList.add('util3');
testScope.appendChild(testElement1);
testScope.appendChild(testElement2);
testScope.appendChild(testElement3);
testScope.appendChild(testElement4);
});
it('should start instances that provide a start function', () => {
utilRegistry.register(TestUtil3);
const initializedInstances = utilRegistry.initAll(testScope);
expect(initializedInstances.length).toBe(2);
});
it('should not start instances that do not provide a start function', () => {
utilRegistry.register(TestUtil1);
utilRegistry.register(TestUtil2);
const startedInstances = utilRegistry.initAll(testScope);
expect(startedInstances.length).toBe(0);
});
});
}); });
}); });
@ -138,6 +186,12 @@ class TestUtil1 {
@Utility({ selector: '#util2' }) @Utility({ selector: '#util2' })
class TestUtil2 { } class TestUtil2 { }
@Utility({ selector: '.util3' })
class TestUtil3 {
constructor() {}
start() {}
}
@Utility({ selector: '#throws' }) @Utility({ selector: '#throws' })
class ThrowingUtil { class ThrowingUtil {
constructor() { constructor() {

View File

@ -3,10 +3,14 @@ import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-mana
import { Datepicker } from '../form/datepicker'; import { Datepicker } from '../form/datepicker';
import { HttpClient } from '../../services/http-client/http-client'; import { HttpClient } from '../../services/http-client/http-client';
import * as debounce from 'lodash.debounce'; import * as debounce from 'lodash.debounce';
import * as throttle from 'lodash.throttle';
import './async-table-filter.sass'; import './async-table-filter.sass';
import './async-table.sass'; import './async-table.sass';
const ATTR_SUBMIT_LOCKED = 'submit-locked';
const INPUT_DEBOUNCE = 600; const INPUT_DEBOUNCE = 600;
const FILTER_DEBOUNCE = 100;
const HEADER_HEIGHT = 80; const HEADER_HEIGHT = 80;
const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE'; const ASYNC_TABLE_LOCAL_STORAGE_KEY = 'ASYNC_TABLE';
@ -33,6 +37,8 @@ export class AsyncTable {
_scrollTable; _scrollTable;
_cssIdPrefix = ''; _cssIdPrefix = '';
_cancelPendingUpdates = [];
_tableFilterInputs = { _tableFilterInputs = {
search: [], search: [],
input: [], input: [],
@ -79,9 +85,6 @@ export class AsyncTable {
throw new Error('Async Table cannot be set up without a scrolltable element!'); throw new Error('Async Table cannot be set up without a scrolltable element!');
} }
this._setupSortableHeaders();
this._setupPagination();
this._setupPageSizeSelect();
this._setupTableFilter(); this._setupTableFilter();
this._processStorage(); this._processStorage();
@ -93,11 +96,18 @@ export class AsyncTable {
this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS); this._element.classList.add(ASYNC_TABLE_INITIALIZED_CLASS);
} }
start() {
this._startSortableHeaders();
this._startPagination();
this._startPageSizeSelect();
this._startTableFilter();
}
destroy() { destroy() {
console.log('TBD: Destroy AsyncTable'); console.log('TBD: Destroy AsyncTable');
} }
_setupSortableHeaders() { _startSortableHeaders() {
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header')) this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
.map((th) => ({ element: th })); .map((th) => ({ element: th }));
@ -110,7 +120,7 @@ export class AsyncTable {
}); });
} }
_setupPagination() { _startPagination() {
const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination'); const pagination = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagination');
if (pagination) { if (pagination) {
this._pageLinks = Array.from(pagination.querySelectorAll('.page-link')) this._pageLinks = Array.from(pagination.querySelectorAll('.page-link'))
@ -134,7 +144,7 @@ export class AsyncTable {
} }
} }
_setupPageSizeSelect() { _startPageSizeSelect() {
// pagesize form // pagesize form
this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form'); this._pagesizeForm = this._element.querySelector('#' + this._cssIdPrefix + this._asyncTableId + '-pagesize-form');
@ -148,70 +158,88 @@ export class AsyncTable {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR); const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) { if (tableFilterForm) {
this._gatherTableFilterInputs(tableFilterForm); this._gatherTableFilterInputs(tableFilterForm);
}
}
_startTableFilter() {
const tableFilterForm = this._element.querySelector(ASYNC_TABLE_FILTER_FORM_SELECTOR);
if (tableFilterForm) {
this._addTableFilterEventListeners(tableFilterForm); this._addTableFilterEventListeners(tableFilterForm);
} }
} }
_gatherTableFilterInputs(tableFilterForm) { _gatherTableFilterInputs(tableFilterForm) {
Array.from(tableFilterForm.querySelectorAll('input[type="search"]')).forEach((input) => { Array.from(tableFilterForm.querySelectorAll('input')).forEach((input) => {
this._tableFilterInputs.search.push(input); const inputType = input.getAttribute('type');
if (inputType === 'search') {
this._tableFilterInputs.search.push(input);
} else if (['text','date','time','datetime-local'].includes(inputType)) {
this._tableFilterInputs.input.push(input);
} else {
this._tableFilterInputs.change.push(input);
}
}); });
Array.from(tableFilterForm.querySelectorAll('input[type="text"]')).forEach((input) => { Array.from(tableFilterForm.querySelectorAll('select')).forEach((input) => this._tableFilterInputs.select.push(input));
this._tableFilterInputs.input.push(input);
});
Array.from(tableFilterForm.querySelectorAll('input:not([type="text"]):not([type="search"])')).forEach((input) => {
this._tableFilterInputs.change.push(input);
});
Array.from(tableFilterForm.querySelectorAll('select')).forEach((input) => {
this._tableFilterInputs.select.push(input);
});
} }
_addTableFilterEventListeners(tableFilterForm) { _addTableFilterEventListeners(tableFilterForm) {
this._tableFilterInputs.search.forEach((input) => { const debouncedUpdateFromTableFilter = throttle((() => this._updateFromTableFilter(tableFilterForm)).bind(this), FILTER_DEBOUNCE, { leading: true, trailing: false });
const debouncedInput = debounce(() => {
if (input.value.length === 0 || input.value.length > 2) { [...this._tableFilterInputs.search, ...this._tableFilterInputs.input].forEach((input) => {
this._updateFromTableFilter(tableFilterForm); const submitLockObserver = new MutationObserver((mutations, observer) => {
} for (const mutation of mutations) {
}, INPUT_DEBOUNCE); // if the submit lock has been released, trigger an update and disconnect this observer
input.addEventListener('input', debouncedInput); if (mutation.target === input && mutation.attributeName === ATTR_SUBMIT_LOCKED && mutation.oldValue === 'true' && mutation.target.getAttribute(mutation.attributeName) === 'false') {
input.addEventListener('input', () => { debouncedUpdateFromTableFilter();
// set flag to ignore any currently pending requests (not debounced) observer.disconnect();
this._ignoreRequest = true; break;
}); }
}); }
});
this._cancelPendingUpdates.push(() => { submitLockObserver.disconnect(); });
this._tableFilterInputs.input.forEach((input) => {
const debouncedInput = debounce(() => { const debouncedInput = debounce(() => {
if (input.value.length === 0 || input.value.length > 2) { const submitLockedAttr = input.getAttribute(ATTR_SUBMIT_LOCKED);
this._updateFromTableFilter(tableFilterForm); const submitLocked = submitLockedAttr === 'true';
if (!submitLocked && (input.value.length === 0 || input.value.length > 2)) {
debouncedUpdateFromTableFilter();
} else if (submitLockedAttr === 'true') {
// observe the submit lock of the input element
submitLockObserver.observe(input, {
attributes: true,
attributeFilter: [ATTR_SUBMIT_LOCKED],
attributeOldValue: true,
});
} }
}, INPUT_DEBOUNCE); }, INPUT_DEBOUNCE);
input.addEventListener('input', debouncedInput); this._cancelPendingUpdates.push(debouncedInput.cancel);
input.addEventListener('input', () => { input.addEventListener('input', () => {
// set flag to ignore any currently pending requests (not debounced)
this._ignoreRequest = true; this._ignoreRequest = true;
debouncedInput();
}); });
}); });
this._tableFilterInputs.change.forEach((input) => { this._tableFilterInputs.change.forEach((input) => {
input.addEventListener('change', () => { input.addEventListener('change', () => {
this._updateFromTableFilter(tableFilterForm); //if (this._element.classList.contains(ASYNC_TABLE_LOADING_CLASS))
this._ignoreRequest = true;
debouncedUpdateFromTableFilter();
}); });
}); });
this._tableFilterInputs.select.forEach((input) => { this._tableFilterInputs.select.forEach((input) => {
input.addEventListener('change', () => { input.addEventListener('change', () => {
this._updateFromTableFilter(tableFilterForm); this._ignoreRequest = true;
debouncedUpdateFromTableFilter();
}); });
}); });
tableFilterForm.addEventListener('submit', (event) =>{ tableFilterForm.addEventListener('submit', (event) =>{
event.preventDefault(); event.preventDefault();
this._updateFromTableFilter(tableFilterForm); this._ignoreRequest = true;
debouncedUpdateFromTableFilter();
}); });
} }
@ -236,15 +264,14 @@ export class AsyncTable {
} }
}; };
} }
this._ignoreRequest = false; this._updateTableFrom(url, callback && callback.bind(this));
this._updateTableFrom(url, callback);
} }
_serializeTableFilterToURL(tableFilterForm) { _serializeTableFilterToURL(tableFilterForm) {
const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href); const url = new URL(this._storageManager.load('currentTableUrl') || window.location.href);
// create new FormData and format any date values // create new FormData and format any date values
const formData = Datepicker.unformatAll(this._massInputForm, new FormData(tableFilterForm)); const formData = Datepicker.unformatAll(tableFilterForm, new FormData(tableFilterForm));
for (var k of url.searchParams.keys()) { for (var k of url.searchParams.keys()) {
url.searchParams.delete(k); url.searchParams.delete(k);
@ -321,6 +348,12 @@ export class AsyncTable {
// fetches new sorted element from url with params and replaces contents of current element // fetches new sorted element from url with params and replaces contents of current element
_updateTableFrom(url, callback) { _updateTableFrom(url, callback) {
const cancelPendingUpdates = (() => {
this._cancelPendingUpdates.forEach(f => f());
}).bind(this);
[0, INPUT_DEBOUNCE * 1.5, FILTER_DEBOUNCE * 1.5].forEach(delay => window.setTimeout(cancelPendingUpdates, delay));
this._ignoreRequest = false;
this._element.classList.add(ASYNC_TABLE_LOADING_CLASS); this._element.classList.add(ASYNC_TABLE_LOADING_CLASS);
const headers = { const headers = {
@ -346,7 +379,7 @@ export class AsyncTable {
// update table with new // update table with new
this._element.innerHTML = response.element.innerHTML; this._element.innerHTML = response.element.innerHTML;
this._app.utilRegistry.setupAll(this._element); this._app.utilRegistry.initAll(this._element);
if (callback && typeof callback === 'function') { if (callback && typeof callback === 'function') {
this._storageManager.save('cssIdPrefix', response.idPrefix); this._storageManager.save('cssIdPrefix', response.idPrefix);

View File

@ -8,7 +8,7 @@ const AppTestMock = {
parseResponse: () => {}, parseResponse: () => {},
}, },
utilRegistry: { utilRegistry: {
setupAll: () => {}, initAll: () => {},
}, },
}; };

View File

@ -93,7 +93,7 @@ export class CheckAll {
th.insertBefore(this._checkAllCheckbox, th.firstChild); th.insertBefore(this._checkAllCheckbox, th.firstChild);
// set up new checkbox // set up new checkbox
this._app.utilRegistry.setupAll(th); this._app.utilRegistry.initAll(th);
this._checkAllCheckbox.addEventListener('input', () => this._onCheckAllCheckboxInput()); this._checkAllCheckbox.addEventListener('input', () => this._onCheckAllCheckboxInput());
this._setupCheckboxListeners(); this._setupCheckboxListeners();

View File

@ -2,7 +2,7 @@ import { CheckAll } from './check-all';
const MOCK_APP = { const MOCK_APP = {
utilRegistry: { utilRegistry: {
setupAll: () => {}, initAll: () => {},
}, },
}; };

View File

@ -6,6 +6,10 @@ import moment from 'moment';
const KEYCODE_ESCAPE = 27; const KEYCODE_ESCAPE = 27;
const Z_INDEX_MODAL = 9999; const Z_INDEX_MODAL = 9999;
// should be the same as ATTR_SUBMIT_LOCKED in async-table util
// TODO move to global config
const ATTR_DATEPICKER_OPEN = 'submit-locked';
// INTERNAL (Uni2work specific) formats for formatting dates and/or times // INTERNAL (Uni2work specific) formats for formatting dates and/or times
const FORM_DATE_FORMAT = { const FORM_DATE_FORMAT = {
'date': moment.HTML5_FMT.DATE, 'date': moment.HTML5_FMT.DATE,
@ -26,21 +30,10 @@ const FORM_DATE_FORMAT_MOMENT = {
'datetime-local': `${FORM_DATE_FORMAT_DATE_MOMENT} ${FORM_DATE_FORMAT_TIME_MOMENT}`, 'datetime-local': `${FORM_DATE_FORMAT_DATE_MOMENT} ${FORM_DATE_FORMAT_TIME_MOMENT}`,
}; };
/**
* Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string.
* If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string;
* @param {*} dateStr string representation of a date (needs to be in format formatIn)
* @param {*} formatIn input format string
* @param {*} formatOut format string of the desired output date string
*/
function reformatDateString(dateStr, formatIn, formatOut) {
const parsedMomentDate = moment(dateStr, [formatIn, formatOut]);
return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr;
}
const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]'; const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';
const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized'; const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';
const DATEPICKER_OPEN_CLASS = 'calendar-open';
const DATEPICKER_CONFIG = { const DATEPICKER_CONFIG = {
'global': { 'global': {
@ -107,13 +100,13 @@ export class Datepicker {
// store the previously set type to select the input format // store the previously set type to select the input format
this.elementType = this._element.getAttribute('type'); this.elementType = this._element.getAttribute('type');
// manually set the type attribute to text because datepicker handles displaying the date
this._element.setAttribute('type', 'text');
// get all relevant config options for this datepicker type // get all relevant config options for this datepicker type
const datepickerGlobalConfig = DATEPICKER_CONFIG['global']; const datepickerGlobalConfig = DATEPICKER_CONFIG['global'];
const datepickerConfig = DATEPICKER_CONFIG[this.elementType]; const datepickerConfig = DATEPICKER_CONFIG[this.elementType];
// manually set the type attribute to text because datepicker handles displaying the date
this._element.setAttribute('type', 'text');
// additional position config (optional data-datepicker-position attribute in html) that can specialize the global config // additional position config (optional data-datepicker-position attribute in html) that can specialize the global config
const datepickerPosition = this._element.dataset.datepickerPosition; const datepickerPosition = this._element.dataset.datepickerPosition;
if (datepickerPosition) { if (datepickerPosition) {
@ -162,7 +155,9 @@ export class Datepicker {
// mark the form input element as initialized // mark the form input element as initialized
this._element.classList.add(DATEPICKER_INITIALIZED_CLASS); this._element.classList.add(DATEPICKER_INITIALIZED_CLASS);
}
start() {
const setDatepickerDate = () => { const setDatepickerDate = () => {
// try to parse the current input element value with fancy and internal format string // try to parse the current input element value with fancy and internal format string
const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]); const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]);
@ -181,6 +176,24 @@ export class Datepicker {
// change the selected date in the tail.datetime instance if the value of the input element is changed // change the selected date in the tail.datetime instance if the value of the input element is changed
this._element.addEventListener('change', setDatepickerDate, { once: true }); this._element.addEventListener('change', setDatepickerDate, { once: true });
// create a mutation observer that observes the datepicker instance class and sets
// the datepicker-open DOM attribute of the input element if the datepicker has been opened
const datepickerInstanceObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!mutation.oldValue.includes(DATEPICKER_OPEN_CLASS) && this.datepickerInstance.dt.getAttribute('class').includes(DATEPICKER_OPEN_CLASS)) {
this._element.setAttribute(ATTR_DATEPICKER_OPEN, true);
break;
}
}
});
datepickerInstanceObserver.observe(this.datepickerInstance.dt, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
});
// close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element // close the instance on focusout of any element if another input is focussed that is neither the timepicker nor _element
window.addEventListener('focusout', event => { window.addEventListener('focusout', event => {
const hasFocus = event.relatedTarget !== null; const hasFocus = event.relatedTarget !== null;
@ -188,7 +201,7 @@ export class Datepicker {
const focussedIsNotElement = event.relatedTarget !== this._element; const focussedIsNotElement = event.relatedTarget !== this._element;
const focussedIsInDocument = window.document.contains(event.relatedTarget); const focussedIsInDocument = window.document.contains(event.relatedTarget);
if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument) if (hasFocus && focussedIsNotTimepicker && focussedIsNotElement && focussedIsInDocument)
this.datepickerInstance.close(); this.closeDatepickerInstance();
}); });
// close the instance on click on any element outside of the datepicker (except the input element itself) // close the instance on click on any element outside of the datepicker (except the input element itself)
@ -198,13 +211,13 @@ export class Datepicker {
const targetIsInDocument = window.document.contains(event.target); const targetIsInDocument = window.document.contains(event.target);
const targetIsNotElement = event.target !== this._element; const targetIsNotElement = event.target !== this._element;
if (targetIsOutside && targetIsInDocument && targetIsNotElement) if (targetIsOutside && targetIsInDocument && targetIsNotElement)
this.datepickerInstance.close(); this.closeDatepickerInstance();
}); });
// close the instance on escape keydown events // close the instance on escape keydown events
this._element.addEventListener('keydown', event => { this._element.addEventListener('keydown', event => {
if (event.keyCode === KEYCODE_ESCAPE) { if (event.keyCode === KEYCODE_ESCAPE) {
this.datepickerInstance.close(); this.closeDatepickerInstance();
} }
}); });
@ -216,6 +229,24 @@ export class Datepicker {
this.datepickerInstance.remove(); this.datepickerInstance.remove();
} }
// DATEPICKER INSTANCE CONTROL
/**
* Closes the datepicker instance, releasing the lock on the input element.
*/
closeDatepickerInstance() {
if (!this._element.datepicker-open) {
throw new Error('Cannot close already closed datepicker instance!');
}
this._element.setAttribute(ATTR_DATEPICKER_OPEN, false);
this.datepickerInstance.close();
}
// FORMAT METHODS
/** /**
* Formats the value of this input element from datepicker format (i.e. DATEPICKER_CONFIG.dateFormat + " " + datetime.defaults.timeFormat) to Uni2work internal date format (i.e. FORM_DATE_FORMAT) required for form submission * Formats the value of this input element from datepicker format (i.e. DATEPICKER_CONFIG.dateFormat + " " + datetime.defaults.timeFormat) to Uni2work internal date format (i.e. FORM_DATE_FORMAT) required for form submission
* @param {*} toFancy optional target format switch (boolean value; default is false). If set to a truthy value, formats the element value to fancy instead of internal date format. * @param {*} toFancy optional target format switch (boolean value; default is false). If set to a truthy value, formats the element value to fancy instead of internal date format.
@ -226,8 +257,6 @@ export class Datepicker {
} }
} }
/** /**
* Returns a datestring in internal format from the current state of the input element value. * Returns a datestring in internal format from the current state of the input element value.
* @param {*} toFancy Format date from internal to fancy or vice versa. When omitted, toFancy is falsy and results in fancy -> internal * @param {*} toFancy Format date from internal to fancy or vice versa. When omitted, toFancy is falsy and results in fancy -> internal
@ -265,3 +294,18 @@ export class Datepicker {
return formData; return formData;
} }
} }
// HELPER FUNCTIONS
/**
* Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string.
* If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string;
* @param {*} dateStr string representation of a date (needs to be in format formatIn)
* @param {*} formatIn input format string
* @param {*} formatOut format string of the desired output date string
*/
function reformatDateString(dateStr, formatIn, formatOut) {
const parsedMomentDate = moment(dateStr, [formatIn, formatOut]);
return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr;
}

View File

@ -4,6 +4,8 @@ import './hide-columns.sass';
const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_HEADER_IDENT = 'uw-hide-column-header';
const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label';
const HIDE_COLUMNS_NO_HIDE = 'uw-hide-columns--no-hide';
const TABLE_UTILS_ATTR = 'table-utils'; const TABLE_UTILS_ATTR = 'table-utils';
const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`;
@ -21,7 +23,7 @@ const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan';
}) })
export class HideColumns { export class HideColumns {
_storageManager = new StorageManager('HIDE_COLUMNS', '1.0.0', { location: LOCATION.LOCAL }); _storageManager = new StorageManager('HIDE_COLUMNS', '1.1.0', { location: LOCATION.LOCAL });
_element; _element;
_elementWrapper; _elementWrapper;
@ -44,8 +46,8 @@ export class HideColumns {
throw new Error('Hide Columns utility cannot be setup without an 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 // do not provide hide-column ability in tables inside modals, async forms with response or tail.datetime instances
if (element.closest('[uw-modal], .async-form__response')) { if (element.closest('[uw-modal], .async-form__response, .tail-datetime-calendar')) {
return false; return false;
} }
@ -66,7 +68,7 @@ export class HideColumns {
hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer); hideColumnsContainer.insertBefore(this._tableUtilContainer, tableContainer);
} }
this._element.querySelectorAll('th').forEach(th => this.setupHideButton(th)); [...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.setupHideButton(th));
} }
setupHideButton(th) { setupHideButton(th) {
@ -80,7 +82,7 @@ export class HideColumns {
const hiderContent = document.createElement('span'); const hiderContent = document.createElement('span');
hiderContent.classList.add('table-hider__label'); hiderContent.classList.add('table-hider__label');
hiderContent.innerHTML = th.innerText; hiderContent.innerHTML = th.getAttribute(HIDE_COLUMNS_HIDER_LABEL) || th.innerText;
hider.appendChild(hiderContent); hider.appendChild(hiderContent);
this.addHeaderHider(th, hider); this.addHeaderHider(th, hider);

View File

@ -155,7 +155,7 @@ export class MassInput {
this._reset(); this._reset();
this._app.utilRegistry.setupAll(this._element); this._app.utilRegistry.initAll(this._element);
} }
_serializeForm(submitButton, enctype) { _serializeForm(submitButton, enctype) {

View File

@ -177,6 +177,6 @@ export class Modal {
this._element.insertBefore(modalContent, null); this._element.insertBefore(modalContent, null);
// setup any newly arrived utils // setup any newly arrived utils
this._app.utilRegistry.setupAll(this._element); this._app.utilRegistry.initAll(this._element);
} }
} }

View File

@ -12,6 +12,8 @@ BtnCourseDeregister: Vom Kurs abmelden
BtnCourseApply: Zum Kurs bewerben BtnCourseApply: Zum Kurs bewerben
BtnCourseRetractApplication: Bewerbung zum Kurs zurückziehen BtnCourseRetractApplication: Bewerbung zum Kurs zurückziehen
BtnExamRegister: Anmelden zur Prüfung BtnExamRegister: Anmelden zur Prüfung
BtnExamRegisterOccurrence: Anmelden zum Prüfungstermin/-raum
BtnExamSwitchOccurrence: Zu Prüfungstermin/-raum wechseln
BtnExamDeregister: Von der Prüfung abmelden BtnExamDeregister: Von der Prüfung abmelden
BtnHijack: Sitzung übernehmen BtnHijack: Sitzung übernehmen
BtnSave: Speichern BtnSave: Speichern
@ -99,6 +101,7 @@ CourseCapacity: Kapazität
CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschränkte Kurskapazität CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschränkte Kurskapazität
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei. CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei. TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei.
ExamOccurrenceNoCapacity: Zu diesem Termin/Raum sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet. CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegistration: Kursanmeldung CourseRegistration: Kursanmeldung
CourseRegisterOpen: Anmeldung möglich CourseRegisterOpen: Anmeldung möglich
@ -409,6 +412,7 @@ UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung e
UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert. UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert.
UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert. UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert.
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung. UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
UnauthorizedExamOccurrenceRegistration: Anmeldung zur Klausur erfolgt nicht inkl. Raum/Termin.
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert. UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
UnauthorizedParticipantSelf: Sie sind kein Teilnehmer dieser Veranstaltung. UnauthorizedParticipantSelf: Sie sind kein Teilnehmer dieser Veranstaltung.
UnauthorizedApplicant: Angegebener Benutzer hat sich nicht für diese Veranstaltung beworben. UnauthorizedApplicant: Angegebener Benutzer hat sich nicht für diese Veranstaltung beworben.
@ -1208,8 +1212,7 @@ BreadcrumbTerm: Semester
BreadcrumbSchool: Institut BreadcrumbSchool: Institut
BreadcrumbUser: Benutzer BreadcrumbUser: Benutzer
BreadcrumbStatic: Statische Resource BreadcrumbStatic: Statische Resource
BreadcrumbFavicon: Favicon BreadcrumbWellKnown: Benannte statische Resource
BreadcrumbRobots: robots.txt
BreadcrumbMetrics: Metriken BreadcrumbMetrics: Metriken
BreadcrumbLecturerInvite: Einladung zum Kursverwalter BreadcrumbLecturerInvite: Einladung zum Kursverwalter
BreadcrumbExamOfficeUserInvite: Einladung bzgl. Prüfungsleistungen BreadcrumbExamOfficeUserInvite: Einladung bzgl. Prüfungsleistungen
@ -1264,6 +1267,8 @@ AuthTagAllocationRegistered: Nutzer nimmt an der Zentralanmeldung teil
AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
AuthTagExamRegistered: Nutzer ist Prüfungsteilnehmer AuthTagExamRegistered: Nutzer ist Prüfungsteilnehmer
AuthTagExamResult: Nutzer hat Prüfungsergebnisse AuthTagExamResult: Nutzer hat Prüfungsergebnisse
AuthTagExamOccurrenceRegistered: Nutzer ist für Prüfungsraum/-termin angemeldet
AuthTagExamOccurrenceRegistration: Anmeldung zur Klausur erfolgt inkl. Raum/Termin
AuthTagParticipant: Nutzer ist mit Kurs assoziiert AuthTagParticipant: Nutzer ist mit Kurs assoziiert
AuthTagApplicant: Nutzer ist mit Bewerber zum Kurs AuthTagApplicant: Nutzer ist mit Bewerber zum Kurs
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
@ -1523,6 +1528,8 @@ ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamBonusManual': Manuelle Berechnung ExamBonusManual': Manuelle Berechnung
ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum
ExamBonusAchieved: Bonuspunkte ExamBonusAchieved: Bonuspunkte
ExamEditHeading examn@ExamName: #{examn} bearbeiten ExamEditHeading examn@ExamName: #{examn} bearbeiten
@ -1535,17 +1542,19 @@ ExamBonusRound: Bonus runden auf
ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positiv und größer null sein ExamBonusRoundNonPositive: Vielfaches, auf das gerundet werden soll, muss positiv und größer null sein
ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet. ExamBonusRoundTip: Bonuspunkte werden kaufmännisch auf ein Vielfaches der angegeben Zahl gerundet.
ExamAutomaticOccurrenceAssignment: Automatische Termin- bzw. Raumzuteilung ExamAutomaticOccurrenceAssignment: Termin- bzw. Raumzuteilung
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. ExamAutomaticOccurrenceAssignmentTip: Sollen Prüfungsteilnehmer automatisch auf die zur Verfügung stehenden Räume bzw. Termine verteilt werden, sich selbstständig einen Raum bzw. Termin aussuchen dürfen oder manuell durch Kursverwalter zugeteilt werden? Manuelle Umverteilung bzw. vorheriges Festlegen von Zuteilungen einzelner Teilnehmer ist trotzdem möglich.
ExamOccurrenceRule: Verfahren ExamOccurrenceRule: Verfahren
ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren ExamOccurrenceRuleParticipant: Termin- bzw. Raumzuteilungsverfahren
ExamRoomManual': Keine automatische Zuteilung ExamRoomManual': Keine automatische bzw. selbstständige Zuteilung
ExamRoomSurname': Nach Nachname ExamRoomSurname': Nach Nachname
ExamRoomMatriculation': Nach Matrikelnummer ExamRoomMatriculation': Nach Matrikelnummer
ExamRoomRandom': Zufällig pro Teilnehmer ExamRoomRandom': Zufällig pro Teilnehmer
ExamRoomFifo': Auswahl durch Teilnehmer bei Anmeldung
ExamOccurrence: Termin/Raum ExamOccurrence: Termin/Raum
ExamNoOccurrence: Kein Termin/Raum ExamNoOccurrence: Kein Termin/Raum
ExamNoSuchOccurrence: Termin/Raum existiert nicht (mehr)
ExamOccurrences: Prüfungen ExamOccurrences: Prüfungen
ExamRooms: Räume ExamRooms: Räume
ExamRoomAlreadyExists: Prüfung ist bereits eingetragen ExamRoomAlreadyExists: Prüfung ist bereits eingetragen
@ -1558,7 +1567,8 @@ ExamRoomStart: Beginn
ExamRoomEnd: Ende ExamRoomEnd: Ende
ExamRoomDescription: Beschreibung ExamRoomDescription: Beschreibung
ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe erfolgt pro Prüfung ExamTimeTip: Nur zur Information der Studierenden, die tatsächliche Zeitangabe erfolgt pro Prüfung
ExamRoomRegistered: Zugeteilt ExamRoomAssigned: Zugeteilt
ExamRoomRegistered: Anmeldung
ExamOccurrenceStart: Prüfungsbeginn ExamOccurrenceStart: Prüfungsbeginn
@ -1604,6 +1614,7 @@ ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Prüfung #{exam} abge
ExamRegistered: Zur Prüfung angemeldet ExamRegistered: Zur Prüfung angemeldet
ExamNotRegistered: Nicht zur Prüfung angemeldet ExamNotRegistered: Nicht zur Prüfung angemeldet
ExamRegistration: Prüfungsanmeldung ExamRegistration: Prüfungsanmeldung
ExamLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2work anmelden
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
@ -1650,6 +1661,8 @@ ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfun
ExamOfficeExamUsersHeading: Prüfungsleistungen ExamOfficeExamUsersHeading: Prüfungsleistungen
ActionsHead: Aktionen
CsvFile: CSV-Datei CsvFile: CSV-Datei
CsvImport: CSV-Import CsvImport: CSV-Import
CsvExport: CSV-Export CsvExport: CSV-Export
@ -1669,6 +1682,7 @@ CsvImportAborted: CSV-Import abgebrochen
CsvImportExplanationLabel: Hinweise zum CSV-Import CsvImportExplanationLabel: Hinweise zum CSV-Import
Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%) Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%)
ProportionNoRatio c@Text of@Text: #{c}/#{of}
CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer

View File

@ -12,6 +12,8 @@ BtnCourseDeregister: Leave course
BtnCourseApply: Apply for course BtnCourseApply: Apply for course
BtnCourseRetractApplication: Retract application BtnCourseRetractApplication: Retract application
BtnExamRegister: Enrol for exam BtnExamRegister: Enrol for exam
BtnExamRegisterOccurrence: Enrol for exam occurrence/room
BtnExamSwitchOccurrence: Switch to exam occurrence/room
BtnExamDeregister: Leave exam BtnExamDeregister: Leave exam
BtnHijack: Hijack session BtnHijack: Hijack session
BtnSave: Save BtnSave: Save
@ -99,6 +101,7 @@ CourseCapacity: Capacity
CourseCapacityTip: Maximum permissable number of enrolments for this course; leave empty for unlimited capacity CourseCapacityTip: Maximum permissable number of enrolments for this course; leave empty for unlimited capacity
CourseNoCapacity: Course has reached maximum capacity CourseNoCapacity: Course has reached maximum capacity
TutorialNoCapacity: Tutorial has reached maximum capacity TutorialNoCapacity: Tutorial has reached maximum capacity
ExamOccurrenceNoCapacity: Occurrence/Room has reached maximum capacity
CourseNotEmpty: There are currently no participants enrolled for this course. CourseNotEmpty: There are currently no participants enrolled for this course.
CourseRegistration: Enrolment CourseRegistration: Enrolment
CourseRegisterOpen: Enrolment is allowed CourseRegisterOpen: Enrolment is allowed
@ -407,6 +410,7 @@ UnauthorizedCorrectorAny: You are no corrector for any course.
UnauthorizedRegistered: You are no participant in this course. UnauthorizedRegistered: You are no participant in this course.
UnauthorizedAllocationRegistered: You are no participant in this central allocation. UnauthorizedAllocationRegistered: You are no participant in this central allocation.
UnauthorizedExamResult: You have no results in this exam. UnauthorizedExamResult: You have no results in this exam.
UnauthorizedExamOccurrenceRegistration: Registration for exam is not done including occurrence/room.
UnauthorizedParticipant: The specified user is no participant of this course. UnauthorizedParticipant: The specified user is no participant of this course.
UnauthorizedParticipantSelf: You are no participant of this course. UnauthorizedParticipantSelf: You are no participant of this course.
UnauthorizedApplicant: The specified user is no applicant for this course. UnauthorizedApplicant: The specified user is no applicant for this course.
@ -1207,8 +1211,7 @@ BreadcrumbTerm: Semester
BreadcrumbSchool: Department BreadcrumbSchool: Department
BreadcrumbUser: User BreadcrumbUser: User
BreadcrumbStatic: Static resource BreadcrumbStatic: Static resource
BreadcrumbFavicon: Favicon BreadcrumbWellKnown: Named static resource
BreadcrumbRobots: robots.txt
BreadcrumbMetrics: Metrics BreadcrumbMetrics: Metrics
BreadcrumbLecturerInvite: Invitation to be a course administrator BreadcrumbLecturerInvite: Invitation to be a course administrator
BreadcrumbExamOfficeUserInvite: Invitation regarding exam achievements BreadcrumbExamOfficeUserInvite: Invitation regarding exam achievements
@ -1263,6 +1266,8 @@ AuthTagAllocationRegistered: User participates in central allocation
AuthTagTutorialRegistered: User is tutorial participant AuthTagTutorialRegistered: User is tutorial participant
AuthTagExamRegistered: User is exam participant AuthTagExamRegistered: User is exam participant
AuthTagExamResult: User has an exam result AuthTagExamResult: User has an exam result
AuthTagExamOccurrenceRegistered: User is registered for exam occurrence/room
AuthTagExamOccurrenceRegistration: Registration for exam is done including occurrence/room
AuthTagParticipant: User participates in course AuthTagParticipant: User participates in course
AuthTagApplicant: User is applicant for course AuthTagApplicant: User is applicant for course
AuthTagRegisterGroup: User is not participant in any tutorial of the same registration group AuthTagRegisterGroup: User is not participant in any tutorial of the same registration group
@ -1521,6 +1526,8 @@ ExamNoBonus': No automatic exam bonus
ExamBonusPoints': Compute from exercise achievements ExamBonusPoints': Compute from exercise achievements
ExamBonusManual': Manual computation ExamBonusManual': Manual computation
ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room
ExamBonusAchieved: Bonus points ExamBonusAchieved: Bonus points
ExamEditHeading examn: Edit #{examn} ExamEditHeading examn: Edit #{examn}
@ -1533,17 +1540,19 @@ ExamBonusRound: Round bonus to
ExamBonusRoundNonPositive: Rounding multiple must be positive and greater than zero ExamBonusRoundNonPositive: Rounding multiple must be positive and greater than zero
ExamBonusRoundTip: Bonus points are rounded commercially to a multiple of the given number ExamBonusRoundTip: Bonus points are rounded commercially to a multiple of the given number
ExamAutomaticOccurrenceAssignment: Automatically assign occurrence/room ExamAutomaticOccurrenceAssignment: Selection of occurrences/rooms for/by participants
ExamAutomaticOccurrenceAssignmentTip: Should exam participants be distributed automatically among the configured occurrences/rooms? Manipulation of the distribution and manually assigning participants remains possible. ExamAutomaticOccurrenceAssignmentTip: Should exam participants be distributed automatically among the configured occurrences/rooms, should they instead be permitted to autonomously choose an occurrence/a room, or should they be assigned to occurrences/rooms manually by course administrators? Manipulation of the distribution and manually assigning participants remains possible.
ExamOccurrenceRule: Procedure ExamOccurrenceRule: Procedure
ExamOccurrenceRuleParticipant: Occurrence/room assignment procedure ExamOccurrenceRuleParticipant: Occurrence/room assignment procedure
ExamRoomManual': No automatic assignment ExamRoomManual': No automatic or autonomous assignment
ExamRoomSurname': By surname ExamRoomSurname': By surname
ExamRoomMatriculation': By matriculation ExamRoomMatriculation': By matriculation
ExamRoomRandom': Randomly ExamRoomRandom': Randomly
ExamRoomFifo': Selected by the participants when registering
ExamOccurrence: Occurrence/room ExamOccurrence: Occurrence/room
ExamNoOccurrence: No occurrence/room ExamNoOccurrence: No occurrence/room
ExamNoSuchOccurrence: Occurrence/Room does not exist (anymore)
ExamOccurrences: Exams ExamOccurrences: Exams
ExamRooms: Rooms ExamRooms: Rooms
ExamRoomAlreadyExists: Occurrence already configured ExamRoomAlreadyExists: Occurrence already configured
@ -1556,7 +1565,8 @@ ExamRoomStart: Start
ExamRoomEnd: End ExamRoomEnd: End
ExamRoomDescription: Description ExamRoomDescription: Description
ExamTimeTip: Only for informational purposes. The actual times are set for each occurrence/room ExamTimeTip: Only for informational purposes. The actual times are set for each occurrence/room
ExamRoomRegistered: Assigned ExamRoomAssigned: Assigned
ExamRoomRegistered: Registration
ExamOccurrenceStart: Exam starts ExamOccurrenceStart: Exam starts
@ -1602,6 +1612,7 @@ ExamDeregisteredSuccess exam: Successufly deregistered from the exam #{exam}
ExamRegistered: Registered for the exam ExamRegistered: Registered for the exam
ExamNotRegistered: Not registered for the exam ExamNotRegistered: Not registered for the exam
ExamRegistration: Exam registration ExamRegistration: Exam registration
ExamLoginToRegister: Your need to login to Uni2work before you can register for this course.
ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register from" ExamRegisterToMustBeAfterRegisterFrom: "Register to" must be after "register from"
ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from" ExamDeregisterUntilMustBeAfterRegisterFrom: "Deregister until" must be after "register from"
@ -1648,6 +1659,8 @@ ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achiev
ExamOfficeExamUsersHeading: Exam achievements ExamOfficeExamUsersHeading: Exam achievements
ActionsHead: Actions
CsvFile: CSV file CsvFile: CSV file
CsvImport: CSV import CsvImport: CSV import
CsvExport: CSV export CsvExport: CSV export
@ -1667,6 +1680,7 @@ CsvImportAborted: CSV import aborted
CsvImportExplanationLabel: Informating regarding CSV import CsvImportExplanationLabel: Informating regarding CSV import
Proportion c of prop: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%) Proportion c of prop: #{c}/#{of} (#{rationalToFixed2 (100 * prop)}%)
ProportionNoRatio c of: #{c}/#{of}
CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants
ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants

View File

@ -3,7 +3,7 @@ Exam
name ExamName name ExamName
gradingRule ExamGradingRule Maybe gradingRule ExamGradingRule Maybe
bonusRule ExamBonusRule Maybe bonusRule ExamBonusRule Maybe
occurrenceRule ExamOccurrenceRule Maybe occurrenceRule ExamOccurrenceRule
visibleFrom UTCTime Maybe visibleFrom UTCTime Maybe
registerFrom UTCTime Maybe registerFrom UTCTime Maybe
registerTo UTCTime Maybe registerTo UTCTime Maybe

747
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "10.1.0", "version": "10.3.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3383,6 +3383,42 @@
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
}
},
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
}
}
},
"babel-code-frame": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -4258,6 +4294,16 @@
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true "dev": true
}, },
"binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
"dev": true,
"requires": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
}
},
"binary-extensions": { "binary-extensions": {
"version": "1.13.1", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
@ -4478,6 +4524,12 @@
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true "dev": true
}, },
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=",
"dev": true
},
"builtin-status-codes": { "builtin-status-codes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@ -4622,6 +4674,16 @@
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true "dev": true
}, },
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"dev": true,
"requires": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"camelcase": { "camelcase": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@ -4746,6 +4808,15 @@
} }
} }
}, },
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
"dev": true,
"requires": {
"traverse": ">=0.3.0 <0.4"
}
},
"chalk": { "chalk": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -4763,6 +4834,76 @@
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
"dev": true "dev": true
}, },
"cheerio": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
"integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
"dev": true,
"requires": {
"css-select": "~1.2.0",
"dom-serializer": "~0.1.0",
"entities": "~1.1.1",
"htmlparser2": "^3.9.1",
"lodash.assignin": "^4.0.9",
"lodash.bind": "^4.1.4",
"lodash.defaults": "^4.0.1",
"lodash.filter": "^4.4.0",
"lodash.flatten": "^4.2.0",
"lodash.foreach": "^4.3.0",
"lodash.map": "^4.4.0",
"lodash.merge": "^4.4.0",
"lodash.pick": "^4.2.1",
"lodash.reduce": "^4.4.0",
"lodash.reject": "^4.4.0",
"lodash.some": "^4.4.0"
},
"dependencies": {
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"dev": true,
"requires": {
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
"dev": true
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
"dev": true,
"requires": {
"domelementtype": "^1.3.0",
"entities": "^1.1.1"
}
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
}
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
}
}
},
"chokidar": { "chokidar": {
"version": "2.1.8", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@ -4843,6 +4984,23 @@
} }
} }
}, },
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
"integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
"dev": true,
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"clean-stack": { "clean-stack": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@ -7193,6 +7351,15 @@
"esutils": "^2.0.2" "esutils": "^2.0.2"
} }
}, },
"dom-converter": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
"integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
"dev": true,
"requires": {
"utila": "~0.4"
}
},
"dom-serialize": { "dom-serialize": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
@ -7235,6 +7402,15 @@
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true "dev": true
}, },
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"dev": true,
"requires": {
"domelementtype": "1"
}
},
"domutils": { "domutils": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@ -8085,6 +8261,17 @@
"chardet": "^0.7.0", "chardet": "^0.7.0",
"iconv-lite": "^0.4.24", "iconv-lite": "^0.4.24",
"tmp": "^0.0.33" "tmp": "^0.0.33"
},
"dependencies": {
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
}
}
} }
}, },
"extglob": { "extglob": {
@ -9030,6 +9217,18 @@
} }
} }
}, },
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"function-bind": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -9499,9 +9698,9 @@
} }
}, },
"glob": { "glob": {
"version": "7.1.4", "version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true, "dev": true,
"requires": { "requires": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
@ -9777,6 +9976,12 @@
"minimalistic-assert": "^1.0.1" "minimalistic-assert": "^1.0.1"
} }
}, },
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"hex-color-regex": { "hex-color-regex": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
@ -9837,6 +10042,133 @@
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"dev": true "dev": true
}, },
"html-minifier": {
"version": "3.5.21",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz",
"integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==",
"dev": true,
"requires": {
"camel-case": "3.0.x",
"clean-css": "4.2.x",
"commander": "2.17.x",
"he": "1.2.x",
"param-case": "2.1.x",
"relateurl": "0.2.x",
"uglify-js": "3.4.x"
},
"dependencies": {
"commander": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
"integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"uglify-js": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
"integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==",
"dev": true,
"requires": {
"commander": "~2.19.0",
"source-map": "~0.6.1"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
"dev": true
}
}
}
}
},
"html-webpack-plugin": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
"integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
"dev": true,
"requires": {
"html-minifier": "^3.2.3",
"loader-utils": "^0.2.16",
"lodash": "^4.17.3",
"pretty-error": "^2.0.2",
"tapable": "^1.0.0",
"toposort": "^1.0.0",
"util.promisify": "1.0.0"
},
"dependencies": {
"big.js": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
"integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==",
"dev": true
},
"json5": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz",
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
"loader-utils": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
"integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=",
"dev": true,
"requires": {
"big.js": "^3.1.3",
"emojis-list": "^2.0.0",
"json5": "^0.5.0",
"object-assign": "^4.0.1"
}
},
"toposort": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz",
"integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=",
"dev": true
}
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"dev": true,
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
},
"dependencies": {
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
},
"readable-stream": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"http-errors": { "http-errors": {
"version": "1.7.2", "version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
@ -10812,6 +11144,15 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true "dev": true
}, },
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
}
},
"to-regex-range": { "to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -11186,24 +11527,96 @@
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
"dev": true "dev": true
}, },
"lodash.assignin": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
"integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=",
"dev": true
},
"lodash.bind": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
"integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=",
"dev": true
},
"lodash.debounce": { "lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true "dev": true
}, },
"lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
"dev": true
},
"lodash.filter": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
"integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=",
"dev": true
},
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
"dev": true
},
"lodash.foreach": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
"integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=",
"dev": true
},
"lodash.ismatch": { "lodash.ismatch": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
"integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
"dev": true "dev": true
}, },
"lodash.map": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=",
"dev": true
},
"lodash.memoize": { "lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true "dev": true
}, },
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=",
"dev": true
},
"lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=",
"dev": true
},
"lodash.reject": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz",
"integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=",
"dev": true
},
"lodash.some": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
"integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
"dev": true
},
"lodash.template": { "lodash.template": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",
@ -11223,6 +11636,11 @@
"lodash._reinterpolate": "~3.0.0" "lodash._reinterpolate": "~3.0.0"
} }
}, },
"lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
},
"lodash.uniq": { "lodash.uniq": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@ -11308,6 +11726,12 @@
"signal-exit": "^3.0.0" "signal-exit": "^3.0.0"
} }
}, },
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=",
"dev": true
},
"lru-cache": { "lru-cache": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
@ -11381,6 +11805,42 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"match-stream": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/match-stream/-/match-stream-0.0.2.tgz",
"integrity": "sha1-mesFAJOzTf+t5CG5rAtBCpz6F88=",
"dev": true,
"requires": {
"buffers": "~0.1.1",
"readable-stream": "~1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
}
},
"matcher": { "matcher": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz",
@ -11518,6 +11978,18 @@
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true "dev": true
}, },
"metaparser": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/metaparser/-/metaparser-1.0.7.tgz",
"integrity": "sha1-wGmaZoageovOGsBrYulGLC5mqso=",
"dev": true,
"requires": {
"async": "*",
"cheerio": "*",
"mkdirp": "*",
"underscore": "*"
}
},
"methods": { "methods": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@ -11822,6 +12294,15 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"dev": true,
"requires": {
"lower-case": "^1.1.1"
}
},
"node-libs-browser": { "node-libs-browser": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@ -11878,6 +12359,46 @@
} }
} }
}, },
"node-unzip-2": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/node-unzip-2/-/node-unzip-2-0.2.8.tgz",
"integrity": "sha512-fmJi73zTRW7RSo/1wyrKc2srKMwb3L6Ppke/7elzQ0QRt6sUjfiIcVsWdrqO5uEHAdvRKXjoySuo4HYe5BB0rw==",
"dev": true,
"requires": {
"binary": "~0.3.0",
"fstream": "~1.0.12",
"match-stream": "~0.0.2",
"pullstream": "~0.4.0",
"readable-stream": "~1.0.0",
"setimmediate": "~1.0.1"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
}
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -15466,6 +15987,12 @@
"integrity": "sha512-Ue462G+UIFoyQmOzapGIKWS3d/9NHeD/018WGEDZIhN2/VaQpVXbofMcZX0socv1fw4/tmEn7Vd3McOdPZfKzQ==", "integrity": "sha512-Ue462G+UIFoyQmOzapGIKWS3d/9NHeD/018WGEDZIhN2/VaQpVXbofMcZX0socv1fw4/tmEn7Vd3McOdPZfKzQ==",
"dev": true "dev": true
}, },
"over": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/over/-/over-0.0.5.tgz",
"integrity": "sha1-8phS5w/X4l82DgE6jsRMgq7bVwg=",
"dev": true
},
"p-defer": { "p-defer": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
@ -15531,6 +16058,15 @@
"readable-stream": "^2.1.5" "readable-stream": "^2.1.5"
} }
}, },
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"dev": true,
"requires": {
"no-case": "^2.2.0"
}
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -16723,6 +17259,16 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true "dev": true
}, },
"pretty-error": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz",
"integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=",
"dev": true,
"requires": {
"renderkid": "^2.0.1",
"utila": "~0.4"
}
},
"private": { "private": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@ -16795,6 +17341,44 @@
"safe-buffer": "^5.1.2" "safe-buffer": "^5.1.2"
} }
}, },
"pullstream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/pullstream/-/pullstream-0.4.1.tgz",
"integrity": "sha1-1vs79a7Wl+gxFQ6xACwlo/iuExQ=",
"dev": true,
"requires": {
"over": ">= 0.0.5 < 1",
"readable-stream": "~1.0.31",
"setimmediate": ">= 1.0.2 < 2",
"slice-stream": ">= 1.0.0 < 2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
}
},
"pump": { "pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -17011,6 +17595,15 @@
"readable-stream": "^2.0.2" "readable-stream": "^2.0.2"
} }
}, },
"real-favicon-webpack-plugin": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/real-favicon-webpack-plugin/-/real-favicon-webpack-plugin-0.2.3.tgz",
"integrity": "sha512-w9CS4DdISimLk+hD1qAqVstWfkAXLpnU7a7UOmArHR1pDnmB4SYBO/fwfuu+ObWEcWtIkfsAhJaD6eKSN4Bq6A==",
"dev": true,
"requires": {
"rfg-api": "^0.5.0"
}
},
"redent": { "redent": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@ -17121,12 +17714,67 @@
} }
} }
}, },
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=",
"dev": true
},
"remove-files-webpack-plugin": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/remove-files-webpack-plugin/-/remove-files-webpack-plugin-1.1.3.tgz",
"integrity": "sha512-r53wQ/IlTkmcv11wri71CZ27S+GhFI5SjHbTbaAJbisPC3qGwg87vlA2C5Z1PuVA+aMI8SgimnE4SqI+ZYzu6Q==",
"dev": true
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"dev": true "dev": true
}, },
"renderkid": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz",
"integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==",
"dev": true,
"requires": {
"css-select": "^1.1.0",
"dom-converter": "^0.2",
"htmlparser2": "^3.3.0",
"strip-ansi": "^3.0.0",
"utila": "^0.4.0"
},
"dependencies": {
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"dev": true,
"requires": {
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
"dev": true
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
}
}
}
},
"repeat-element": { "repeat-element": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
@ -17373,6 +18021,19 @@
"integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==", "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==",
"dev": true "dev": true
}, },
"rfg-api": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/rfg-api/-/rfg-api-0.5.0.tgz",
"integrity": "sha512-wd6BcVoBsEHlbfsaB1WD4Z/Xis4uEP+Qctd3u2jxXR5yTkCYENaB/m3Jsk0G2qToKgAeE+6tbsN6T0n8DHcSaw==",
"dev": true,
"requires": {
"axios": "^0.18.0",
"fstream": "^1.0.2",
"metaparser": "^1.0.7",
"mkdirp": "^0.5.0",
"node-unzip-2": "^0.2.7"
}
},
"rgb-regex": { "rgb-regex": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
@ -17770,6 +18431,41 @@
"is-fullwidth-code-point": "^2.0.0" "is-fullwidth-code-point": "^2.0.0"
} }
}, },
"slice-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slice-stream/-/slice-stream-1.0.0.tgz",
"integrity": "sha1-WzO9ZvATsaf4ZGCwPUY97DmtPqA=",
"dev": true,
"requires": {
"readable-stream": "~1.0.31"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
"dev": true
}
}
},
"snapdragon": { "snapdragon": {
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
@ -18970,12 +19666,12 @@
"dev": true "dev": true
}, },
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
"dev": true, "dev": true,
"requires": { "requires": {
"os-tmpdir": "~1.0.2" "rimraf": "^2.6.3"
} }
}, },
"to-array": { "to-array": {
@ -19067,6 +19763,12 @@
} }
} }
}, },
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=",
"dev": true
},
"trim-newlines": { "trim-newlines": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
@ -19181,6 +19883,12 @@
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==",
"dev": true "dev": true
}, },
"underscore": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz",
"integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@ -19315,6 +20023,12 @@
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==",
"dev": true "dev": true
}, },
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
"dev": true
},
"uri-js": { "uri-js": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
@ -19368,6 +20082,17 @@
"requires": { "requires": {
"lru-cache": "4.1.x", "lru-cache": "4.1.x",
"tmp": "0.0.x" "tmp": "0.0.x"
},
"dependencies": {
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.2"
}
}
} }
}, },
"util": { "util": {
@ -19395,6 +20120,12 @@
"object.getownpropertydescriptors": "^2.0.3" "object.getownpropertydescriptors": "^2.0.3"
} }
}, },
"utila": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
"integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=",
"dev": true
},
"utils-merge": { "utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "10.1.0", "version": "10.3.0",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -74,6 +74,8 @@
"css-loader": "^2.1.1", "css-loader": "^2.1.1",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"file-loader": "^5.0.2", "file-loader": "^5.0.2",
"glob": "^7.1.6",
"html-webpack-plugin": "^3.2.0",
"husky": "^2.7.0", "husky": "^2.7.0",
"jasmine-core": "^3.5.0", "jasmine-core": "^3.5.0",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
@ -92,6 +94,8 @@
"optimize-css-assets-webpack-plugin": "^5.0.3", "optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"real-favicon-webpack-plugin": "^0.2.3",
"remove-files-webpack-plugin": "^1.1.3",
"resolve-url-loader": "^3.1.1", "resolve-url-loader": "^3.1.1",
"sass": "^1.23.7", "sass": "^1.23.7",
"sass-loader": "^7.3.1", "sass-loader": "^7.3.1",
@ -99,6 +103,7 @@
"standard-version": "^6.0.1", "standard-version": "^6.0.1",
"style-loader": "^0.23.1", "style-loader": "^0.23.1",
"terser-webpack-plugin": "^2.2.3", "terser-webpack-plugin": "^2.2.3",
"tmp": "^0.1.0",
"typeface-roboto": "0.0.75", "typeface-roboto": "0.0.75",
"typeface-source-sans-pro": "0.0.75", "typeface-source-sans-pro": "0.0.75",
"webpack": "^4.41.2", "webpack": "^4.41.2",
@ -110,6 +115,7 @@
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"@juggle/resize-observer": "^2.5.0", "@juggle/resize-observer": "^2.5.0",
"core-js": "^3.4.8", "core-js": "^3.4.8",
"lodash.throttle": "^4.1.1",
"moment": "^2.24.0", "moment": "^2.24.0",
"npm": "^6.13.4", "npm": "^6.13.4",
"tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#master", "tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#master",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 10.1.0 version: 10.3.0
dependencies: dependencies:
- base - base

View File

@ -739,5 +739,31 @@
"usedIds": [] "usedIds": []
} }
} }
],
"app-manifest-webpack-plugin for \"/home/gkleen/projects/uni2work/static/wp-4.41/iconstats-[hash].json\"": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
],
"app-manifest-webpack-plugin for \"../../config/favicon.json\"": [
{
"modules": {
"byIdentifier": {},
"usedIds": {}
},
"chunks": {
"byName": {},
"bySource": {},
"usedIds": []
}
}
] ]
} }

6
routes
View File

@ -39,8 +39,6 @@
/static StaticR EmbeddedStatic appStatic !free /static StaticR EmbeddedStatic appStatic !free
/auth AuthR Auth getAuth !free /auth AuthR Auth getAuth !free
/favicon.ico FaviconR GET !free
/robots.txt RobotsR GET !free
/metrics MetricsR GET /metrics MetricsR GET
/ HomeR GET !free / HomeR GET !free
@ -175,6 +173,7 @@
/users/new EAddUserR GET POST /users/new EAddUserR GET POST
/users/invite EInviteR GET POST /users/invite EInviteR GET POST
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result /register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result
/register/#ExamOccurrenceName ERegisterOccR POST !exam-occurrence-registrationANDtimeANDcapacityANDcourse-registeredAND¬exam-occurrence-registered !exam-occurrence-registrationANDtimeANDexam-occurrence-registeredAND¬exam-result
/grades EGradesR GET POST !exam-office /grades EGradesR GET POST !exam-office
/apps CApplicationsR GET POST /apps CApplicationsR GET POST
!/apps/files CAppsFilesR GET !/apps/files CAppsFilesR GET
@ -204,6 +203,7 @@
/msgs MessageListR GET POST /msgs MessageListR GET POST
/msg/#{CryptoUUIDSystemMessage} MessageR GET POST !timeANDreadANDauthentication /msg/#{CryptoUUIDSystemMessage} MessageR GET POST !timeANDreadANDauthentication
!/#UUID CryptoUUIDDispatchR GET !free -- just redirect !/#UUID CryptoUUIDDispatchR GET !free -- just redirect
-- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists -- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists
!/*WellKnownFileName WellKnownR GET !free

View File

@ -19,7 +19,7 @@ let
''; '';
override = oldAttrs: { override = oldAttrs: {
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap google-chrome ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install weeder ]); nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap google-chrome exiftool ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install weeder ]);
shellHook = '' shellHook = ''
export PROMPT_INFO="${oldAttrs.name}" export PROMPT_INFO="${oldAttrs.name}"

View File

@ -101,7 +101,6 @@ import Data.List (cycle)
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Common
import Handler.Home import Handler.Home
import Handler.Info import Handler.Info
import Handler.Help import Handler.Help
@ -194,7 +193,7 @@ makeFoundation appSettings'@AppSettings{..} = do
runAppLoggingT tempFoundation $ do runAppLoggingT tempFoundation $ do
$logInfoS "InstanceID" $ UUID.toText appInstanceID $logInfoS "InstanceID" $ UUID.toText appInstanceID
-- logDebugS "Configuration" $ tshow appSettings' $logDebugS "Configuration" $ tshow appSettings'
smtpPool <- for appSmtpConf $ \c -> do smtpPool <- for appSmtpConf $ \c -> do
$logDebugS "setup" "SMTP-Pool" $logDebugS "setup" "SMTP-Pool"

View File

@ -504,9 +504,9 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn
cTime <- liftIO getCurrentTime cTime <- liftIO getCurrentTime
registered <- case mAuthId of registration <- case mAuthId of
Just uid -> $cachedHereBinary (eId, uid) . lift . existsBy $ UniqueExamRegistration eId uid Just uid -> $cachedHereBinary (eId, uid) . lift . getBy $ UniqueExamRegistration eId uid
Nothing -> return False Nothing -> return Nothing
let visible = NTop examVisibleFrom <= NTop (Just cTime) let visible = NTop examVisibleFrom <= NTop (Just cTime)
@ -515,11 +515,23 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
EUsersR -> guard $ NTop examStart <= NTop (Just cTime) EUsersR -> guard $ NTop examStart <= NTop (Just cTime)
&& NTop (Just cTime) <= NTop examFinished && NTop (Just cTime) <= NTop examFinished
ERegisterR ERegisterR
| not registered -> guard $ visible | is _Nothing registration
&& NTop examRegisterFrom <= NTop (Just cTime) -> guard $ visible
&& NTop (Just cTime) <= NTop examRegisterTo && NTop examRegisterFrom <= NTop (Just cTime)
| otherwise -> guard $ visible && NTop (Just cTime) <= NTop examRegisterTo
&& NTop (Just cTime) <= NTop examDeregisterUntil | otherwise
-> guard $ visible
&& NTop (Just cTime) <= NTop examDeregisterUntil
ERegisterOccR occn -> do
occId <- (>>= hoistMaybe) . $cachedHereBinary (eId, occn) . lift . getKeyBy $ UniqueExamOccurrence eId occn
if
| (registration >>= examRegistrationOccurrence . entityVal) == Just occId
-> guard $ visible
&& NTop (Just cTime) <= NTop examDeregisterUntil
| otherwise
-> guard $ visible
&& NTop examRegisterFrom <= NTop (Just cTime)
&& NTop (Just cTime) <= NTop examRegisterTo
_ -> return () _ -> return ()
return Authorized return Authorized
@ -758,6 +770,59 @@ tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case rout
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTutorialRegistered r r -> $unsupportedAuthPredicate AuthTutorialRegistered r
tagAccessPredicate AuthExamOccurrenceRegistration = APDB $ \_ route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do
isOccurrenceRegistration <- $cachedHereBinary (tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam) -> do
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
E.&&. exam E.^. ExamOccurrenceRule E.==. E.val ExamRoomFifo
guardMExceptT isOccurrenceRegistration (unauthorizedI MsgUnauthorizedExamOccurrenceRegistration)
return Authorized
r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistration r
tagAccessPredicate AuthExamOccurrenceRegistered = APDB $ \mAuthId route _ -> case route of
CExamR tid ssh csh examn (ERegisterOccR occn) -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn, occn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration `E.InnerJoin` examOccurrence) -> do
E.on $ E.just (examOccurrence E.^. ExamOccurrenceId) E.==. examRegistration E.^. ExamRegistrationOccurrence
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
E.&&. examOccurrence E.^. ExamOccurrenceName E.==. E.val occn
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
E.&&. E.not_ (E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence)
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. E.not_ (E.isNothing $ examRegistration E.^. ExamRegistrationOccurrence)
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistered r
tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
@ -768,7 +833,7 @@ tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn E.&&. exam E.^. ExamName E.==. E.val examn
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered) guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
@ -933,6 +998,13 @@ tagAccessPredicate AuthApplicant = APDB $ \mAuthId route _ -> case route of
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
tagAccessPredicate AuthCapacity = APDB $ \_ route _ -> case route of tagAccessPredicate AuthCapacity = APDB $ \_ route _ -> case route of
CExamR tid ssh csh examn (ERegisterOccR occn) -> maybeT (unauthorizedI MsgExamOccurrenceNoCapacity) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
eid <- $cachedHereBinary (cid, examn) . MaybeT . getKeyBy $ UniqueExam cid examn
Entity occId ExamOccurrence{..} <- $cachedHereBinary (eid, occn) . MaybeT . getBy $ UniqueExamOccurrence eid occn
registered <- $cachedHereBinary occId . lift $ fromIntegral <$> count [ ExamRegistrationOccurrence ==. Just occId, ExamRegistrationExam ==. eid ]
guard $ examOccurrenceCapacity > registered
return Authorized
CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgTutorialNoCapacity) $ do CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgTutorialNoCapacity) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity tutId Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn Entity tutId Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
@ -1574,18 +1646,9 @@ siteLayout' headingOverride widget = do
frontendDatetimeLocale <- toJSON <$> selectLanguage frontendDatetimeLocales frontendDatetimeLocale <- toJSON <$> selectLanguage frontendDatetimeLocales
pc <- widgetToPageContent $ do pc <- widgetToPageContent $ do
$logDebugS "siteLayout" $ tshow webpackEntrypoint_main webpackLinks_main StaticR
forM_ webpackEntrypoint_main $ \(sRoute, mime) ->
let ctEq = (==) `on` simpleContentType
in if
| mime `ctEq` "text/css"
-> addStylesheet $ StaticR sRoute
| mime `ctEq` "application/javascript"
-> addScript $ StaticR sRoute
| otherwise
-> $logErrorS "siteLayout" [st|Unknown mime type in webpack bundle: #{tshow mime}|]
toWidget $(juliusFile "templates/i18n.julius") toWidget $(juliusFile "templates/i18n.julius")
wellKnownHtmlLinks
$(widgetFile "default-layout") $(widgetFile "default-layout")
withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet")
@ -1636,8 +1699,7 @@ i18nCrumb msg mbR = do
instance YesodBreadcrumbs UniWorX where instance YesodBreadcrumbs UniWorX where
breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR breadcrumb (AuthR _) = i18nCrumb MsgMenuLogin $ Just HomeR
breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing breadcrumb (StaticR _) = i18nCrumb MsgBreadcrumbStatic Nothing
breadcrumb FaviconR = i18nCrumb MsgBreadcrumbFavicon Nothing breadcrumb (WellKnownR _) = i18nCrumb MsgBreadcrumbWellKnown Nothing
breadcrumb RobotsR = i18nCrumb MsgBreadcrumbRobots Nothing
breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing breadcrumb MetricsR = i18nCrumb MsgBreadcrumbMetrics Nothing
breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing breadcrumb HomeR = i18nCrumb MsgMenuHome Nothing
@ -1801,6 +1863,7 @@ instance YesodBreadcrumbs UniWorX where
ECInviteR -> i18nCrumb MsgBreadcrumbExamCorrectorInvite . Just $ CExamR tid ssh csh examn EShowR ECInviteR -> i18nCrumb MsgBreadcrumbExamCorrectorInvite . Just $ CExamR tid ssh csh examn EShowR
EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR EInviteR -> i18nCrumb MsgBreadcrumbExamParticipantInvite . Just $ CExamR tid ssh csh examn EShowR
ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR ERegisterR -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR
ERegisterOccR _occn -> i18nCrumb MsgBreadcrumbExamRegister . Just $ CExamR tid ssh csh examn EShowR
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do

View File

@ -1,29 +0,0 @@
-- | Common handler functions.
module Handler.Common
( getFaviconR
, getRobotsR
) where
import Data.FileEmbed (embedFile)
import Import hiding (embedFile)
-- These handlers embed files in the executable at compile time to avoid a
-- runtime dependency, and for efficiency.
getFaviconR :: Handler TypedContent
getFaviconR = do
let content = $(embedFile "static/favicon.ico")
setEtagHashable content
return $ TypedContent "image/x-icon"
$ toContent content
getRobotsR :: Handler TypedContent
getRobotsR = do
let content = $(embedFile "static/robots.txt")
setEtagHashable content
return $ TypedContent typePlain
$ toContent content

View File

@ -114,6 +114,8 @@ getCShowR tid ssh csh = do
-> return . modal $(widgetFile "course/login-to-register") . Left . SomeRoute $ AuthR LoginR -> return . modal $(widgetFile "course/login-to-register") . Left . SomeRoute $ AuthR LoginR
registrationOpen <- hasWriteAccessTo $ CourseR tid ssh csh CRegisterR registrationOpen <- hasWriteAccessTo $ CourseR tid ssh csh CRegisterR
MsgRenderer mr <- getMsgRenderer
let let
tutorialDBTable = DBTable{..} tutorialDBTable = DBTable{..}
where where
@ -151,7 +153,7 @@ getCShowR tid ssh csh = do
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
in return $ E.val tutorialCapacity' E.-. numParticipants in return $ E.val tutorialCapacity' E.-. numParticipants
return . toWidget $ tshow freeCapacity return . toWidget $ tshow freeCapacity
, sortable Nothing mempty $ \DBRow{ dbrOutput = Entity tutId Tutorial{..} } -> sqlCell $ do , sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgActionsHead)) $ \DBRow{ dbrOutput = Entity tutId Tutorial{..} } -> sqlCell $ do
mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True mayRegister <- (== Authorized) <$> evalAccessDB (CTutorialR tid ssh csh tutorialName TRegisterR) True
isRegistered <- case mbAid of isRegistered <- case mbAid of
Nothing -> return False Nothing -> return False

View File

@ -39,7 +39,7 @@ data ExamForm = ExamForm
, efPublicStatistics :: Bool , efPublicStatistics :: Bool
, efGradingRule :: Maybe ExamGradingRule , efGradingRule :: Maybe ExamGradingRule
, efBonusRule :: Maybe ExamBonusRule , efBonusRule :: Maybe ExamBonusRule
, efOccurrenceRule :: Maybe ExamOccurrenceRule , efOccurrenceRule :: ExamOccurrenceRule
, efCorrectors :: Set (Either UserEmail UserId) , efCorrectors :: Set (Either UserEmail UserId)
, efExamParts :: Set ExamPartForm , efExamParts :: Set ExamPartForm
} }
@ -96,7 +96,7 @@ examForm template html = do
<*> apopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (efPublicStatistics <$> template <|> Just True) <*> apopt checkBoxField (fslI MsgExamPublicStatistics & setTooltip MsgExamPublicStatisticsTip) (efPublicStatistics <$> template <|> Just True)
<*> optionalActionA (examGradingRuleForm $ efGradingRule =<< template) (fslI MsgExamAutomaticGrading & setTooltip MsgExamAutomaticGradingTip) (is _Just . efGradingRule <$> template) <*> optionalActionA (examGradingRuleForm $ efGradingRule =<< template) (fslI MsgExamAutomaticGrading & setTooltip MsgExamAutomaticGradingTip) (is _Just . efGradingRule <$> template)
<*> optionalActionA (examBonusRuleForm $ efBonusRule =<< template) (fslI MsgExamBonus) (is _Just . efBonusRule <$> template) <*> optionalActionA (examBonusRuleForm $ efBonusRule =<< template) (fslI MsgExamBonus) (is _Just . efBonusRule <$> template)
<*> optionalActionA (examOccurrenceRuleForm $ efOccurrenceRule =<< template) (fslI MsgExamAutomaticOccurrenceAssignment & setTooltip MsgExamAutomaticOccurrenceAssignmentTip) (is _Just . efOccurrenceRule <$> template) <*> (examOccurrenceRuleForm $ efOccurrenceRule <$> template)
<* aformSection MsgExamFormCorrection <* aformSection MsgExamFormCorrection
<*> examCorrectorsForm (efCorrectors <$> template) <*> examCorrectorsForm (efCorrectors <$> template)
<* aformSection MsgExamFormParts <* aformSection MsgExamFormParts

View File

@ -1,6 +1,7 @@
module Handler.Exam.Register module Handler.Exam.Register
( ButtonExamRegister(..) ( ButtonExamRegister(..)
, postERegisterR , postERegisterR
, postERegisterOccR
) where ) where
import Import import Import
@ -8,45 +9,84 @@ import Import
import Handler.Utils import Handler.Utils
import Handler.Utils.Exam import Handler.Utils.Exam
import Database.Persist.Sql (deleteWhereCount)
-- Dedicated ExamRegistrationButton -- Dedicated ExamRegistrationButton
data ButtonExamRegister = BtnExamRegister | BtnExamDeregister data ButtonExamRegister = BtnExamRegisterOccurrence
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) | BtnExamSwitchOccurrence
| BtnExamRegister
| BtnExamDeregister
deriving (Enum, Bounded, Eq, Ord, Read, Show, Generic, Typeable)
instance Universe ButtonExamRegister instance Universe ButtonExamRegister
instance Finite ButtonExamRegister instance Finite ButtonExamRegister
nullaryPathPiece ''ButtonExamRegister $ camelToPathPiece' 1 nullaryPathPiece ''ButtonExamRegister $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''ButtonExamRegister id
instance Button UniWorX ButtonExamRegister where
btnClasses BtnExamRegister = [BCIsButton, BCPrimary]
btnClasses BtnExamDeregister = [BCIsButton, BCDanger]
btnLabel BtnExamRegister = [whamlet|#{iconExamRegister True} _{MsgBtnExamRegister}|] instance Button UniWorX ButtonExamRegister where
btnLabel BtnExamDeregister = [whamlet|#{iconExamRegister False} _{MsgBtnExamDeregister}|] btnClasses BtnExamRegisterOccurrence = [BCIsButton, BCPrimary]
btnClasses BtnExamSwitchOccurrence = [BCIsButton, BCPrimary]
btnClasses BtnExamRegister = [BCIsButton, BCPrimary]
btnClasses BtnExamDeregister = [BCIsButton, BCDanger]
btnLabel BtnExamRegisterOccurrence = [whamlet|#{iconExamRegister True } _{MsgBtnExamRegisterOccurrence}|]
btnLabel BtnExamSwitchOccurrence = [whamlet|_{MsgBtnExamSwitchOccurrence}|]
btnLabel BtnExamRegister = [whamlet|#{iconExamRegister True } _{MsgBtnExamRegister}|]
btnLabel BtnExamDeregister = [whamlet|#{iconExamRegister False} _{MsgBtnExamDeregister}|]
postERegisterR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html postERegisterR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
postERegisterR tid ssh csh examn = do postERegisterR tid ssh csh examn = do
Entity uid User{..} <- requireAuth Entity uid User{..} <- requireAuth
Entity eId Exam{..} <- runDB $ fetchExam tid ssh csh examn Entity eId Exam{..} <- runDB $ fetchExam tid ssh csh examn
((btnResult, _), _) <- runFormPost buttonForm ((btnResult, _), _) <- runFormPost $ buttonForm' [BtnExamRegister, BtnExamDeregister]
formResult btnResult $ \case formResult btnResult $ \case
BtnExamDeregister -> do
runDB $ do
deleted <- deleteWhereCount [ExamRegistrationExam ==. eId, ExamRegistrationUser ==. uid]
unless (deleted <= 0) $
audit $ TransactionExamDeregister eId uid
addMessageIconI Success IconExamRegisterFalse $ MsgExamDeregisteredSuccess examn
redirect $ CExamR tid ssh csh examn EShowR
BtnExamRegister -> do BtnExamRegister -> do
runDB $ do runDB $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
insert_ $ ExamRegistration eId uid Nothing now void $ upsertBy (UniqueExamRegistration eId uid) (ExamRegistration eId uid Nothing now) [ExamRegistrationTime =. now]
audit $ TransactionExamRegister eId uid audit $ TransactionExamRegister eId uid
addMessageIconI Success IconExamRegisterTrue $ MsgExamRegisteredSuccess examn addMessageIconI Success IconExamRegisterTrue $ MsgExamRegisteredSuccess examn
redirect $ CExamR tid ssh csh examn EShowR redirect $ CExamR tid ssh csh examn EShowR
_other -> error "Unexpected due to definition of buttonForm'"
redirect $ CExamR tid ssh csh examn EShowR
postERegisterOccR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> ExamOccurrenceName -> Handler Html
postERegisterOccR tid ssh csh examn occn = do
Entity uid User{..} <- requireAuth
(Entity eId Exam{..}, Entity occId ExamOccurrence{..}) <- runDB $ do
eexam@(Entity eId _) <- fetchExam tid ssh csh examn
occ <- getBy404 $ UniqueExamOccurrence eId occn
return (eexam, occ)
((btnResult, _), _) <- runFormPost buttonForm
formResult btnResult $ \case
BtnExamDeregister -> do BtnExamDeregister -> do
runDB $ do runDB $ do
deleteBy $ UniqueExamRegistration eId uid deleted <- deleteWhereCount [ExamRegistrationExam ==. eId, ExamRegistrationUser ==. uid]
audit $ TransactionExamDeregister eId uid unless (deleted <= 0) $
addMessageIconI Info IconExamRegisterFalse $ MsgExamDeregisteredSuccess examn audit $ TransactionExamDeregister eId uid
-- yes, it's a success message, but it should be visually different from a positive success, since most will just note the positive green color! See discussion on commit 5f4925a4 addMessageIconI Success IconExamRegisterFalse $ MsgExamDeregisteredSuccess examn
redirect $ CExamR tid ssh csh examn EShowR redirect $ CExamR tid ssh csh examn EShowR
btn | btn `elem` [BtnExamRegisterOccurrence, BtnExamSwitchOccurrence] -> do
runDB $ do
now <- liftIO getCurrentTime
void $ upsertBy (UniqueExamRegistration eId uid) (ExamRegistration eId uid (Just occId) now) [ExamRegistrationOccurrence =. Just occId, ExamRegistrationTime =. now]
audit $ TransactionExamRegister eId uid
addMessageIconI Success IconExamRegisterTrue $ MsgExamRegisteredSuccess examn
redirect $ CExamR tid ssh csh examn EShowR
_other -> error "Unexpected due to definition of buttonForm'"
redirect $ CExamR tid ssh csh examn EShowR
invalidArgs ["Register/Deregister button required"]

View File

@ -30,7 +30,7 @@ getEShowR tid ssh csh examn = do
let gradingVisible = NTop (Just cTime) >= NTop examFinished let gradingVisible = NTop (Just cTime) >= NTop examFinished
gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR gradingShown <- or2M (return gradingVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments let occurrenceAssignmentsVisible = NTop (Just cTime) >= NTop examPublishOccurrenceAssignments || examOccurrenceRule == ExamRoomFifo
occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR occurrenceAssignmentsShown <- or2M (return occurrenceAssignmentsVisible) . hasReadAccessTo $ CExamR tid ssh csh examn EEditR
examParts <- sortOn (view $ _entityVal . _examPartNumber) <$> selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ] examParts <- sortOn (view $ _entityVal . _examPartNumber) <$> selectList [ ExamPartExam ==. eId ] [ Asc ExamPartName ]
@ -58,10 +58,16 @@ getEShowR tid ssh csh examn = do
E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom] E.orderBy [E.desc registered, E.asc $ examOccurrence E.^. ExamOccurrenceStart, E.asc $ examOccurrence E.^. ExamOccurrenceRoom]
return (examOccurrence, registered) return (examOccurrence, registered)
let occurrences = map (over _2 E.unValue) occurrencesRaw registered <- for mUid $ getBy . UniqueExamRegistration eId
mayRegister <- if
| examOccurrenceRule == ExamRoomFifo -> anyM occurrencesRaw $ \(Entity _ ExamOccurrence{..}, _) ->
hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName
| otherwise -> hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR
registered <- for mUid $ existsBy . UniqueExamRegistration eId let occurrences = sortOn sortPred $ map (over _2 E.unValue) occurrencesRaw
mayRegister <- (== Authorized) <$> evalAccessDB (CExamR tid ssh csh examName ERegisterR) True where
sortPred (Entity _ ExamOccurrence{..}, registered')
= (Down $ registered' && not mayRegister, examOccurrenceStart, examOccurrenceRoom)
lecturerInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR lecturerInfoShown <- hasReadAccessTo $ CExamR tid ssh csh examn EEditR
@ -84,11 +90,17 @@ getEShowR tid ssh csh examn = do
hasRegistration = any snd occurrences hasRegistration = any snd occurrences
mayRegister' <- fmap ((Map.!) . Map.fromList) . for (Nothing : map Just occurrences) $ \case
Nothing ->
fmap (Nothing, ) . hasWriteAccessTo $ CExamR tid ssh csh examName ERegisterR
Just (Entity occId ExamOccurrence{..}, _) ->
fmap (Just occId, ) . hasWriteAccessTo . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
registerWidget registerWidget mOcc
| Just isRegistered <- registered | isRegistered <- is _Just $ join registered
, mayRegister = Just $ do , examOccurrenceRule /= ExamRoomFifo || (isRegistered && not (any snd occurrences))
, mayRegister' (entityKey <$> mOcc) = Just $ do
(examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered (examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [BtnExamRegister] [BtnExamDeregister] isRegistered
[whamlet| [whamlet|
<p> <p>
@ -102,11 +114,37 @@ getEShowR tid ssh csh examn = do
, formEncoding = examRegisterEnctype , formEncoding = examRegisterEnctype
, formSubmit = FormNoSubmit , formSubmit = FormNoSubmit
} }
| fromMaybe False registered = Just [whamlet|_{MsgExamRegistered}|] | examOccurrenceRule == ExamRoomFifo
, Just (Entity occId ExamOccurrence{..}) <- mOcc
, isRegistered <- (== Just occId) $ examRegistrationOccurrence . entityVal =<< join registered
, mayRegister' (Just occId) = Just $ do
(examRegisterForm, examRegisterEnctype) <- liftHandler . generateFormPost . buttonForm' $ bool [bool BtnExamRegisterOccurrence BtnExamSwitchOccurrence . is _Just $ join registered] [BtnExamDeregister] isRegistered
wrapForm examRegisterForm def
{ formAction = Just . SomeRoute . CExamR tid ssh csh examName $ ERegisterOccR examOccurrenceName
, formEncoding = examRegisterEnctype
, formSubmit = FormNoSubmit
}
| is _Nothing mOcc
, is _Nothing registered
= Just [whamlet|_{MsgExamLoginToRegister}|]
| is _Nothing mOcc
, isRegistered <- is _Just $ join registered
= Just
[whamlet|
<p>
$if isRegistered
_{MsgExamRegistered}
$else
_{MsgExamNotRegistered}
$if mayRegister
^{messageTooltip =<< messageI Info MsgExamRegisterForOccurrence}
|]
| otherwise = Nothing | otherwise = Nothing
showMaxPoints = any (has $ _entityVal . _examPartMaxPoints . _Just) examParts showMaxPoints = any (has $ _entityVal . _examPartMaxPoints . _Just) examParts
showAchievedPoints = not $ null results showAchievedPoints = not $ null results
showOccurrenceRegisterColumn = occurrenceAssignmentsShown || (mayRegister && examOccurrenceRule == ExamRoomFifo)
markUnregisteredOccurrences mOcc = occurrenceAssignmentsShown && hasRegistration && isn't _Just (registerWidget mOcc)
let heading = prependCourseTitle tid ssh csh $ CI.original examName let heading = prependCourseTitle tid ssh csh $ CI.original examName

View File

@ -104,6 +104,7 @@ getMaterialListR tid ssh csh = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
seeAllModificationTimestamps <- hasWriteAccessTo $ CourseR tid ssh csh MaterialNewR -- ordinary users should not see modification dates older than visibility seeAllModificationTimestamps <- hasWriteAccessTo $ CourseR tid ssh csh MaterialNewR -- ordinary users should not see modification dates older than visibility
MsgRenderer mr <- getMsgRenderer
table <- runDB $ do table <- runDB $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
let row2material = view $ _dbrOutput . _1 . _entityVal let row2material = view $ _dbrOutput . _1 . _entityVal
@ -127,9 +128,9 @@ getMaterialListR tid ssh csh = do
$ foldMap (textCell . CI.original) . materialType . row2material $ foldMap (textCell . CI.original) . materialType . row2material
, sortable (Just "name") (i18nCell MsgMaterialName) , sortable (Just "name") (i18nCell MsgMaterialName)
$ liftA2 anchorCell matLink toWgt . materialName . row2material $ liftA2 anchorCell matLink toWgt . materialName . row2material
, sortable (toNothingS "description") mempty , sortable (toNothingS "description") (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgMaterialDescription))
$ foldMap modalCell . materialDescription . row2material $ foldMap modalCell . materialDescription . row2material
, sortable (toNothingS "zip-archive") mempty , sortable (toNothingS "zip-archive") (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgMaterialFiles))
$ \DBRow{ dbrOutput = (Entity _ Material{..}, E.Value fileNum) } -> if $ \DBRow{ dbrOutput = (Entity _ Material{..}, E.Value fileNum) } -> if
| fileNum == 0 -> mempty | fileNum == 0 -> mempty
| otherwise -> fileCell $ filesLink materialName | otherwise -> fileCell $ filesLink materialName

View File

@ -16,6 +16,7 @@ import qualified Data.CaseInsensitive as CI
getCTutorialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCTutorialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCTutorialListR tid ssh csh = do getCTutorialListR tid ssh csh = do
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
MsgRenderer mr <- getMsgRenderer
let let
tutorialDBTable = DBTable{..} tutorialDBTable = DBTable{..}
@ -51,7 +52,7 @@ getCTutorialListR tid ssh csh = do
, sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterFrom , sortable (Just "register-from") (i18nCell MsgTutorialRegisterFrom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterFrom
, sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterTo , sortable (Just "register-to") (i18nCell MsgTutorialRegisterTo) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialRegisterTo
, sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialDeregisterUntil , sortable (Just "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialDeregisterUntil
, sortable Nothing mempty $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> cell $ do , sortable Nothing (mempty & cellAttrs <>~ pure ("uw-hide-columns--hider-label", mr MsgActionsHead)) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> cell $ do
linkButton mempty [whamlet|_{MsgTutorialEdit}|] [BCIsButton] . SomeRoute $ CTutorialR tid ssh csh tutorialName TEditR linkButton mempty [whamlet|_{MsgTutorialEdit}|] [BCIsButton] . SomeRoute $ CTutorialR tid ssh csh tutorialName TEditR
linkButton mempty [whamlet|_{MsgTutorialDelete}|] [BCIsButton, BCDanger] . SomeRoute $ CTutorialR tid ssh csh tutorialName TDeleteR linkButton mempty [whamlet|_{MsgTutorialDelete}|] [BCIsButton, BCDanger] . SomeRoute $ CTutorialR tid ssh csh tutorialName TDeleteR
] ]

View File

@ -71,6 +71,7 @@ instance Button UniWorX AllUsersAction where
getUsersR, postUsersR :: Handler Html getUsersR, postUsersR :: Handler Html
getUsersR = postUsersR getUsersR = postUsersR
postUsersR = do postUsersR = do
MsgRenderer mr <- getMsgRenderer
let let
dbtColonnade = mconcat $ dbtColonnade = mconcat $
[ dbRow [ dbRow
@ -100,7 +101,7 @@ postUsersR = do
$forall (E.Value sh) <- schools $forall (E.Value sh) <- schools
<li>#{sh} <li>#{sh}
|] |]
, sortable Nothing mempty $ \inp@DBRow{ dbrOutput = Entity uid _ } -> FormCell , sortable Nothing (mempty & cellAttrs <>~ pure ("hide-columns--hider-label", mr MsgActionsHead)) $ \inp@DBRow{ dbrOutput = Entity uid _ } -> FormCell
{ formCellAttrs = [] { formCellAttrs = []
, formCellLens = id , formCellLens = id
, formCellContents = do , formCellContents = do

View File

@ -554,7 +554,9 @@ examBonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classify
) )
] ]
data ExamOccurrenceRule' = ExamRoomSurname' data ExamOccurrenceRule' = ExamRoomManual'
| ExamRoomFifo'
| ExamRoomSurname'
| ExamRoomMatriculation' | ExamRoomMatriculation'
| ExamRoomRandom' | ExamRoomRandom'
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -566,14 +568,18 @@ embedRenderMessage ''UniWorX ''ExamOccurrenceRule' id
classifyExamOccurrenceRule :: ExamOccurrenceRule -> ExamOccurrenceRule' classifyExamOccurrenceRule :: ExamOccurrenceRule -> ExamOccurrenceRule'
classifyExamOccurrenceRule = \case classifyExamOccurrenceRule = \case
ExamRoomManual -> ExamRoomManual'
ExamRoomSurname -> ExamRoomSurname' ExamRoomSurname -> ExamRoomSurname'
ExamRoomMatriculation -> ExamRoomMatriculation' ExamRoomMatriculation -> ExamRoomMatriculation'
ExamRoomRandom -> ExamRoomRandom' ExamRoomRandom -> ExamRoomRandom'
ExamRoomFifo -> ExamRoomFifo'
examOccurrenceRuleForm :: Maybe ExamOccurrenceRule -> AForm Handler ExamOccurrenceRule examOccurrenceRuleForm :: Maybe ExamOccurrenceRule -> AForm Handler ExamOccurrenceRule
examOccurrenceRuleForm = fmap reverseClassify . areq (selectField optionsFinite) (fslI MsgExamOccurrenceRule) . fmap classifyExamOccurrenceRule examOccurrenceRuleForm = fmap reverseClassify . areq (selectField optionsFinite) (fslI MsgExamAutomaticOccurrenceAssignment & setTooltip MsgExamAutomaticOccurrenceAssignmentTip) . fmap classifyExamOccurrenceRule
where where
reverseClassify = \case reverseClassify = \case
ExamRoomManual' -> ExamRoomManual
ExamRoomFifo' -> ExamRoomFifo
ExamRoomSurname' -> ExamRoomSurname ExamRoomSurname' -> ExamRoomSurname
ExamRoomMatriculation' -> ExamRoomMatriculation ExamRoomMatriculation' -> ExamRoomMatriculation
ExamRoomRandom' -> ExamRoomRandom ExamRoomRandom' -> ExamRoomRandom

View File

@ -197,7 +197,9 @@ numCell :: (IsDBTable m a, ToMessage b) => b -> DBCell m a
numCell = textCell . toMessage numCell = textCell . toMessage
propCell :: (IsDBTable m a, Real b, ToMessage b) => b -> b -> DBCell m a propCell :: (IsDBTable m a, Real b, ToMessage b) => b -> b -> DBCell m a
propCell curr max' = i18nCell $ MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max') propCell curr max'
| max' /= 0 = i18nCell $ MsgProportion (toMessage curr) (toMessage max') (toRational curr / toRational max')
| otherwise = i18nCell $ MsgProportionNoRatio (toMessage curr) (toMessage max')
int64Cell :: IsDBTable m a => Int64-> DBCell m a int64Cell :: IsDBTable m a => Int64-> DBCell m a
int64Cell = numCell int64Cell = numCell

View File

@ -1382,7 +1382,7 @@ dbSelect :: forall x h r i a. (Headedness h, Monoid' x)
-> (DBRow r -> MForm (HandlerFor UniWorX) i) -> (DBRow r -> MForm (HandlerFor UniWorX) i)
-> Colonnade h (DBRow r) (DBCell (MForm (HandlerFor UniWorX)) x) -> Colonnade h (DBRow r) (DBCell (MForm (HandlerFor UniWorX)) x)
-- dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ formCell resLens genIndex genForm -- dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ i18nCell MsgSelectColumn) $ formCell resLens genIndex genForm
dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty) $ formCell resLens genIndex genForm dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty & cellAttrs <>~ pure ("uw-hide-columns--no-hide","")) $ formCell resLens genIndex genForm
where where
genForm _ mkUnique = do genForm _ mkUnique = do
(selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False) (selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False)

View File

@ -12,8 +12,9 @@ import Utils.Tokens as Import
import Utils.Frontend.Modal as Import import Utils.Frontend.Modal as Import
import Utils.Lens as Import import Utils.Lens as Import
import Settings as Import import Settings as Import
import Settings.StaticFiles as Import import Settings.StaticFiles as Import
import Settings.WellKnownFiles as Import
import CryptoID as Import import CryptoID as Import
import Audit as Import import Audit as Import

View File

@ -599,6 +599,13 @@ customMigrations = Map.fromListWith (>>)
ALTER TABLE "study_features" DROP COLUMN "sub_field"; ALTER TABLE "study_features" DROP COLUMN "sub_field";
|] |]
) )
, ( AppliedMigrationKey [migrationVersion|29.0.0|] [version|30.0.0|]
, whenM (tableExists "exam") $
[executeQQ|
UPDATE "exam" SET "occurrence_rule" = #{ExamRoomManual} WHERE "occurrence_rule" IS NULL;
ALTER TABLE "exam" ALTER COLUMN "occurrence_rule" SET NOT NULL;
|]
)
] ]

View File

@ -136,7 +136,9 @@ deriveJSON defaultOptions
} ''ExamBonusRule } ''ExamBonusRule
derivePersistFieldJSON ''ExamBonusRule derivePersistFieldJSON ''ExamBonusRule
data ExamOccurrenceRule = ExamRoomSurname data ExamOccurrenceRule = ExamRoomManual
| ExamRoomFifo
| ExamRoomSurname
| ExamRoomMatriculation | ExamRoomMatriculation
| ExamRoomRandom | ExamRoomRandom
deriving (Show, Read, Eq, Ord, Generic, Typeable) deriving (Show, Read, Eq, Ord, Generic, Typeable)

View File

@ -54,6 +54,8 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthCourseRegistered | AuthCourseRegistered
| AuthTutorialRegistered | AuthTutorialRegistered
| AuthExamRegistered | AuthExamRegistered
| AuthExamOccurrenceRegistered
| AuthExamOccurrenceRegistration
| AuthExamResult | AuthExamResult
| AuthParticipant | AuthParticipant
| AuthApplicant | AuthApplicant

View File

@ -78,6 +78,8 @@ data AppSettings = AppSettings
{ appStaticDir :: FilePath { appStaticDir :: FilePath
-- ^ Directory from which to serve static files. -- ^ Directory from which to serve static files.
, appWebpackEntrypoints :: FilePath , appWebpackEntrypoints :: FilePath
, appWellKnownDir :: FilePath
, appWellKnownLinkFile :: FilePath
, appDatabaseConf :: PostgresConf , appDatabaseConf :: PostgresConf
-- ^ Configuration settings for accessing the database. -- ^ Configuration settings for accessing the database.
, appAutoDbMigrate :: Bool , appAutoDbMigrate :: Bool
@ -369,6 +371,8 @@ instance FromJSON AppSettings where
False False
#endif #endif
appStaticDir <- o .: "static-dir" appStaticDir <- o .: "static-dir"
appWellKnownDir <- o .: "well-known-dir"
appWellKnownLinkFile <- o .: "well-known-link-file"
appWebpackEntrypoints <- o .: "webpack-manifest" appWebpackEntrypoints <- o .: "webpack-manifest"
appDatabaseConf <- o .: "database" appDatabaseConf <- o .: "database"
appAutoDbMigrate <- o .: "auto-db-migrate" appAutoDbMigrate <- o .: "auto-db-migrate"
@ -393,7 +397,7 @@ instance FromJSON AppSettings where
appMailFrom <- o .: "mail-from" appMailFrom <- o .: "mail-from"
appMailObjectDomain <- o .: "mail-object-domain" appMailObjectDomain <- o .: "mail-object-domain"
appMailVerp <- o .: "mail-verp" appMailVerp <- fromMaybe VerpNone . join <$> (o .:? "mail-verp" <|> pure Nothing)
appMailSupport <- o .: "mail-support" appMailSupport <- o .: "mail-support"
appJobWorkers <- o .: "job-workers" appJobWorkers <- o .: "job-workers"

View File

@ -1,9 +1,15 @@
{-# OPTIONS_GHC -fno-warn-unused-top-binds #-}
-- Listing only files directly used by consumers of this module
-- prevents rebuilds if files change, that are not directly used (like
-- webpack bundles)
module Settings.StaticFiles module Settings.StaticFiles
( module Settings.StaticFiles ( img_lmu_sigillum_svg
, webpackLinks_main
, embeddedStatic
, module Yesod.EmbeddedStatic , module Yesod.EmbeddedStatic
) where ) where
import ClassyPrelude import ClassyPrelude.Yesod
import Settings (appStaticDir, appWebpackEntrypoints, compileTimeAppSettings) import Settings (appStaticDir, appWebpackEntrypoints, compileTimeAppSettings)
import Settings.StaticFiles.Generator import Settings.StaticFiles.Generator

View File

@ -12,7 +12,7 @@ import qualified Data.Yaml as Yaml
import qualified Data.Map as Map import qualified Data.Map as Map
import Yesod.Core (Route) import Yesod.Core (Route, MonadLogger, MonadWidget, HandlerSite, logDebugS, logErrorS)
import Yesod.EmbeddedStatic (EmbeddedStatic) import Yesod.EmbeddedStatic (EmbeddedStatic)
import Yesod.EmbeddedStatic.Types import Yesod.EmbeddedStatic.Types
import Network.Mime (MimeType) import Network.Mime (MimeType)
@ -27,6 +27,8 @@ import Utils (nubOn)
import System.FilePath (makeRelative) import System.FilePath (makeRelative)
import Text.Shakespeare.Text (st)
mkWebpackEntrypoints :: FilePath -- ^ Path to YAML-manifest mkWebpackEntrypoints :: FilePath -- ^ Path to YAML-manifest
-> [FilePath -> Generator] -> [FilePath -> Generator]
@ -54,11 +56,30 @@ mkWebpackEntrypoints manifest mkGen stDir = do
Just n -> tell $ pure (n, ebMimeType entry) Just n -> tell $ pure (n, ebMimeType entry)
let entryName = mkName $ "webpackEntrypoint_" <> entrypoint let entryName = mkName $ "webpackEntrypoint_" <> entrypoint
widgetName = mkName $ "webpackLinks_" <> entrypoint
staticR <- newName "staticR"
sequence sequence
[ sigD entryName [t|[(Route EmbeddedStatic, MimeType)]|] [ sigD entryName [t|[(Route EmbeddedStatic, MimeType)]|]
, funD entryName , funD entryName
[ clause [] (normalB . listE . map (\(n, mime) -> tupE [varE n, TH.lift mime]) $ nubOn fst entries) [] [ clause [] (normalB . listE . map (\(n, mime) -> tupE [varE n, TH.lift mime]) $ nubOn fst entries) []
] ]
, sigD widgetName [t|forall m. (MonadLogger m, MonadWidget m) => (Route EmbeddedStatic -> Route (HandlerSite m)) -> m ()|]
, funD widgetName
[ clause [varP staticR] (normalB [e|
do
$logDebugS "siteLayout" $ tshow $(varE entryName)
forM_ $(varE entryName) $ \(sRoute, mime) ->
let ctEq = (==) `on` simpleContentType
in if
| mime `ctEq` "text/css"
-> addStylesheet $ $(varE staticR) sRoute
| mime `ctEq` "application/javascript"
-> addScript $ $(varE staticR) sRoute
| otherwise
-> $logErrorS "siteLayout" [st|Unknown mime type in webpack bundle: #{tshow mime}|]
|]) []
]
] ]
where where
decodeManifest :: FilePath -> Q (Map String [FilePath]) decodeManifest :: FilePath -> Q (Map String [FilePath])

View File

@ -0,0 +1,11 @@
module Settings.WellKnownFiles
( WellKnownFileName(..)
, getWellKnownR
, wellKnownHtmlLinks
) where
import Settings.WellKnownFiles.TH
import Settings (appWellKnownDir, appWellKnownLinkFile, compileTimeAppSettings)
mkWellKnown "de-de-formal" (appWellKnownDir compileTimeAppSettings) (appWellKnownLinkFile compileTimeAppSettings)

View File

@ -0,0 +1,192 @@
module Settings.WellKnownFiles.TH
( mkWellKnown
) where
import ClassyPrelude.Yesod
import Utils
import Language.Haskell.TH
import Language.Haskell.TH.Syntax hiding (Lift(..))
import qualified Language.Haskell.TH.Syntax as TH (Lift(..))
import System.Directory.Tree
import qualified Data.ByteString as BS
import Utils.Lens.TH
import Control.Lens
import Data.Set.Lens
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Char as Char (isAlphaNum, toUpper)
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.List.NonEmpty (NonEmpty(..))
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import System.FilePath ((</>), splitDirectories, makeRelative)
import Settings.Mime
import Text.Blaze.Html (preEscapedToHtml)
nWellKnownFileName :: Name
nWellKnownFileName = mkName "WellKnownFileName"
nwellKnownFileNames :: Name
nwellKnownFileNames = mkName "wellKnownFileNames"
ngetWellKnownR :: Name
ngetWellKnownR = mkName "getWellKnownR"
nwellKnownHtmlLinks :: Name
nwellKnownHtmlLinks = mkName "wellKnownHtmlLinks"
mkWellKnown :: Lang -- ^ Default language
-> FilePath -- ^ Base directory
-> FilePath -- ^ Link file (@html_code.html@)
-> DecsQ
mkWellKnown defLang wellKnownBase wellKnownLinks = do
inputFiles <- fmap dirTree . liftIO $ readDirectoryWith (\f -> (f, ) <$> BS.readFile f) wellKnownBase
mapM_ qAddDependentFile $ inputFiles ^.. folded . _1
-- languageFiles :: Map Lang [(FilePath, ByteString)]
languageFiles <- if
| Dir{contents} <- inputFiles
-> return . Map.fromList $ do
(language, lContents) <- contents ^.. folded . $(multifocusL 2) _name id
Dir{} <- pure lContents
let
lContents' :: [(FilePath, ByteString)]
lContents' = flip mapMaybe (flattenDir lContents) $ \pFile -> do
File{..} <- pure pFile
guard $ name /= wellKnownLinks
return $ file & _1 %~ makeRelative (wellKnownBase </> language)
return (Text.pack language, lContents')
| otherwise
-> fail "wellKnownBase is not a directory"
fLanguages <- if
| defLang `Set.member` Map.keysSet languageFiles
, let languages' = Set.delete defLang $ Map.keysSet languageFiles
-> return $ defLang :| Set.toList languages'
| otherwise
-> fail "default language is missing in wellKnownBase"
-- languageLinks :: Map Lang ByteString
languageLinks <- if
| Dir{contents} <- inputFiles
-> return . Map.fromList $ do
(language, lContents) <- contents ^.. folded . $(multifocusL 2) _name id
Dir{} <- pure lContents
let
lContents' :: [ByteString]
lContents' = flip mapMaybe (flattenDir lContents) $ \pFile -> do
File{..} <- pure pFile
guard $ name == wellKnownLinks
return $ file ^. _2
c <- lContents'
return (Text.pack language, c)
| otherwise
-> fail "wellKnownBase is not a directory"
lLanguages <- if
| defLang `Set.member` Map.keysSet languageLinks
, let languages' = Set.delete defLang $ Map.keysSet languageLinks
-> return $ defLang :| Set.toList languages'
| otherwise
-> fail "default language is missing in wellKnownBase"
fVar <- newName "f"
hVar <- newName "h"
lVar <- newName "l"
let fileNames = setOf (folded . folded . _1) languageFiles
fileContents = Map.fromListWith (<>) $ do
(lang, fs) <- Map.toList languageFiles
(fName, fContent) <- fs
return ((fContent, mimeLookup $ Text.pack fName), Set.singleton (lang, fName))
wellKnownFileName = dataD
(cxt [])
nWellKnownFileName
[]
Nothing
[ normalC (mkName $ fNameManip fName) []
| fName <- Set.toList fileNames
]
(pure $ derivClause Nothing [[t|Eq|], [t|Ord|], [t|Bounded|], [t|Enum|], [t|Read|], [t|Show|], [t|Generic|], [t|Typeable|]])
wellKnownFileNameMapSig = sigD
nwellKnownFileNames
[t|HashMap [Text] $(conT nWellKnownFileName)|]
wellKnownFileNameMap = funD
nwellKnownFileNames
[ clause [] (normalB $ [e|HashMap.fromList|] `appE` listE [ [e|($(TH.lift . map Text.pack $ splitDirectories fName), $(conE . mkName $ fNameManip fName))|] | fName <- Set.toList fileNames ]) []
]
wellKnownFileNamePathMultiPiece = instanceD
(cxt [])
(conT ''PathMultiPiece `appT` conT nWellKnownFileName)
[ funD 'toPathMultiPiece
[ clause [conP (mkName $ fNameManip fName) []] (normalB . TH.lift . map Text.pack $ splitDirectories fName) []
| fName <- Set.toList fileNames
]
, funD 'fromPathMultiPiece $
[ clause [] (normalB [e|flip HashMap.lookup $(varE nwellKnownFileNames)|]) []
]
]
wellKnownFileNameHashable = instanceD
(cxt [])
(conT ''Hashable `appT` conT nWellKnownFileName)
[]
getWellKnownRSig = sigD
ngetWellKnownR
[t|forall m. MonadHandler m => $(conT nWellKnownFileName) -> m TypedContent|]
getWellKnownR = funD
ngetWellKnownR
[ clause [varP fVar] (normalB [e|$(varE hVar) =<< selectLanguage fLanguages|])
[ funD hVar $
[ clause [varP lVar] (guardedB
[ (,) <$> normalG [e|HashSet.member ($(varE lVar), $(varE fVar)) $ HashSet.fromList $(listE [ tupE [TH.lift l, conE . mkName $ fNameManip fName] | (l, fName) <- Set.toList xs ])|]
<*> [e|TypedContent mime (toContent fContent) <$ setEtag $(TH.lift $ hashToText (mime, fContent))|]
]) []
| ((fContent, mime), xs) <- Map.toList fileContents
] ++ pure (clause [wildP] (normalB [e|notFound|]) [])
]
]
wellKnownHtmlLinksSig = sigD
nwellKnownHtmlLinks
[t|forall m. MonadWidget m => m ()|]
wellKnownHtmlLinks = funD
nwellKnownHtmlLinks
[ clause [] (normalB [e|toWidgetHead . preEscapedToHtml . $(varE hVar) =<< selectLanguage lLanguages|])
[ sigD hVar [t|Text -> Text|]
, funD hVar $
[ clause [varP lVar] (guardedB
[ (,) <$> normalG [|$(varE lVar) == lang|]
<*> TH.lift (Text.filter (`notElem` ['\r', '\n']) $ Text.decodeUtf8 c)
]) []
| (lang, c) <- Map.toList languageLinks
] ++ pure (clause [wildP] (normalB [e|mempty|]) [])
]
]
sequence
[ wellKnownFileName, wellKnownFileNameMapSig, wellKnownFileNameMap, wellKnownFileNamePathMultiPiece, wellKnownFileNameHashable
, getWellKnownRSig, getWellKnownR
, wellKnownHtmlLinksSig, wellKnownHtmlLinks
]
where
fNameManip = Text.unpack . mconcat . over (traverse . _head) Char.toUpper . filter (not . null) . Text.split (not . isAlphaNum) . Text.pack

View File

@ -913,11 +913,11 @@ cachedHereBinary = do
[e| \k -> cachedByBinary (loc, k) |] [e| \k -> cachedByBinary (loc, k) |]
hashToText :: Hashable a => a -> Text hashToText :: Hashable a => a -> Text
hashToText = decodeUtf8 . Base64.encode . toStrict . Binary.encode . hash hashToText = Text.dropWhileEnd (== '=') . decodeUtf8 . Base64.encode . toStrict . Binary.encode . hash
setEtagHashable, setWeakEtagHashable :: (MonadHandler m, Hashable a) => a -> m () setEtagHashable, setWeakEtagHashable :: (MonadHandler m, Hashable a) => a -> m ()
setEtagHashable = setEtag . hashToText setEtagHashable = setEtag . hashToText
setWeakEtagHashable = setEtag . hashToText setWeakEtagHashable = setWeakEtag . hashToText
setLastModified :: MonadHandler m => UTCTime -> m () setLastModified :: MonadHandler m => UTCTime -> m ()
setLastModified lastModified = do setLastModified lastModified = do

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@ -76,15 +76,15 @@ $maybe desc <- examDescription
\ ^{isVisible False} \ ^{isVisible False}
<dd .deflist__dd> <dd .deflist__dd>
^{examBonusW bonusRule} ^{examBonusW bonusRule}
$maybe occurrenceRule <- examOccurrenceRule $if examOccurrenceRule /= ExamRoomManual
$if occurrenceAssignmentsShown $if occurrenceAssignmentsShown
<dt .deflist__dt> <dt .deflist__dt>
_{MsgExamOccurrenceRuleParticipant} _{MsgExamOccurrenceRuleParticipant}
$if not occurrenceAssignmentsVisible $if not occurrenceAssignmentsVisible
\ ^{isVisible False} \ ^{isVisible False}
<dd .deflist__dd> <dd .deflist__dd>
_{classifyExamOccurrenceRule occurrenceRule} _{classifyExamOccurrenceRule examOccurrenceRule}
$maybe registerWdgt <- registerWidget $maybe registerWdgt <- registerWidget Nothing
<dt .deflist__dt>_{MsgExamRegistration} <dt .deflist__dt>_{MsgExamRegistration}
<dd .deflist__dd>^{registerWdgt} <dd .deflist__dd>^{registerWdgt}
@ -103,31 +103,39 @@ $if not (null occurrences)
<th .table__th> <th .table__th>
_{MsgExamRoomName} _{MsgExamRoomName}
\ ^{isVisible False} \ ^{isVisible False}
$if occurrenceAssignmentsShown
<th .table__th>
_{MsgExamRoomRegistered}
$if not occurrenceAssignmentsVisible
\ ^{isVisible False}
<th .table__th>_{MsgExamRoom} <th .table__th>_{MsgExamRoom}
$if not examTimes $if not examTimes
<th .table__th>_{MsgExamRoomTime} <th .table__th>_{MsgExamRoomTime}
$if showOccurrenceRegisterColumn
<th .table__th>
$if examOccurrenceRule == ExamRoomFifo
_{MsgExamRoomRegistered}
$else
_{MsgExamRoomAssigned}
$if not occurrenceAssignmentsVisible
\ ^{isVisible False}
<th .table__th>_{MsgExamRoomDescription} <th .table__th>_{MsgExamRoomDescription}
<tbody> <tbody>
$forall (Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences $forall (occurrence, registered) <- occurrences
<tr .table__row :occurrenceAssignmentsShown && (not registered && hasRegistration):.occurrence--not-registered> $with Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription} <- occurrence
$if occurrenceNamesShown $with registerWdgt <- registerWidget (Just occurrence)
<td .table__td #exam-occurrence__#{examOccurrenceName}>#{examOccurrenceName} <tr .table__row :markUnregisteredOccurrences (Just occurrence) && not registered:.occurrence--not-registered>
$if occurrenceAssignmentsShown $if occurrenceNamesShown
<td .table__td> <td .table__td #exam-occurrence__#{examOccurrenceName}>#{examOccurrenceName}
$if registered <td .table__td>#{examOccurrenceRoom}
#{iconOK} $if not examTimes
<td .table__td>#{examOccurrenceRoom} <td .table__td>
$if not examTimes ^{formatTimeRangeW SelFormatDateTime examOccurrenceStart examOccurrenceEnd}
<td .table__td> $if showOccurrenceRegisterColumn
^{formatTimeRangeW SelFormatDateTime examOccurrenceStart examOccurrenceEnd} <td .table__td>
<td .table__td> $maybe registerWdgt' <- registerWdgt
$maybe desc <- examOccurrenceDescription ^{registerWdgt'}
#{desc} $nothing
$if registered
#{iconOK}
<td .table__td>
$maybe desc <- examOccurrenceDescription
#{desc}
$if gradingShown && not (null examParts) $if gradingShown && not (null examParts)
<section> <section>

View File

@ -1,7 +1,7 @@
$newline never $newline never
<div .tooltip :isInlineTooltip:.tooltip__inline> <span .tooltip :isInlineTooltip:.tooltip__inline>
<div .tooltip__handle .#{urgency}> <span .tooltip__handle .#{urgency}>
<i .fas .fa-^{ic}> <i .fas .fa-^{ic}>
<div .tooltip__content> <span .tooltip__content>
^{tooltip} ^{tooltip}

View File

@ -504,7 +504,7 @@ fillDb = do
, examName = "Klausur" , examName = "Klausur"
, examGradingRule = Nothing , examGradingRule = Nothing
, examBonusRule = Nothing , examBonusRule = Nothing
, examOccurrenceRule = Nothing , examOccurrenceRule = ExamRoomManual
, examVisibleFrom = Just now , examVisibleFrom = Just now
, examRegisterFrom = Just now , examRegisterFrom = Just now
, examRegisterTo = Just $ addUTCTime (14 * nominalDay) now , examRegisterTo = Just $ addUTCTime (14 * nominalDay) now

View File

@ -25,6 +25,11 @@ instance Arbitrary (Route EmbeddedStatic) where
params <- replicateM paramNum $ (,) <$> printableText' <*> printableText params <- replicateM paramNum $ (,) <$> printableText' <*> printableText
return $ embeddedResourceR path params return $ embeddedResourceR path params
instance Arbitrary WellKnownFileName where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary SchoolR where instance Arbitrary SchoolR where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink

View File

@ -6,12 +6,12 @@ spec :: Spec
spec = withApp $ do spec = withApp $ do
describe "robots.txt" $ do describe "robots.txt" $ do
it "gives a 200" $ do it "gives a 200" $ do
get RobotsR get $ WellKnownR RobotsTxt
statusIs 200 statusIs 200
it "has correct User-agent" $ do it "has correct User-agent" $ do
get RobotsR get $ WellKnownR RobotsTxt
bodyContains "User-agent: *" bodyContains "User-agent: *"
describe "favicon.ico" $ do describe "favicon.ico" $ do
it "gives a 200" $ do it "gives a 200" $ do
get FaviconR get $ WellKnownR FaviconIco
statusIs 200 statusIs 200

View File

@ -53,6 +53,7 @@ import Control.Monad.Catch as X hiding (Handler(..))
import Control.Monad.Trans.Resource (runResourceT) import Control.Monad.Trans.Resource (runResourceT)
import Settings import Settings
import Settings.WellKnownFiles as X
import Data.CaseInsensitive as X (CI) import Data.CaseInsensitive as X (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI

View File

@ -1,5 +1,11 @@
const webpack = require('webpack'); const webpack = require('webpack');
const path = require('path'); const path = require('path');
const tmp = require('tmp');
tmp.setGracefulCleanup();
const fs = require('fs');
const glob = require('glob');
const { execSync } = require('child_process');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin'); const ManifestPlugin = require('webpack-manifest-plugin');
@ -9,6 +15,8 @@ const TerserPlugin = require('terser-webpack-plugin');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const HashOutput = require('webpack-plugin-hash-output'); const HashOutput = require('webpack-plugin-hash-output');
const postcssPresetEnv = require('postcss-preset-env'); const postcssPresetEnv = require('postcss-preset-env');
const RemovePlugin = require('remove-files-webpack-plugin');
const RealFaviconPlugin = require('real-favicon-webpack-plugin');
const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.'); const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.');
const packageVersion = require('./package.json').version; const packageVersion = require('./package.json').version;
@ -17,57 +25,57 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
plugins: ['syntax-dynamic-import'], plugins: ['syntax-dynamic-import'],
presets: [ presets: [
[ [
'@babel/preset-env', '@babel/preset-env',
{ {
modules: false, modules: false,
targets: { targets: {
edge: "17", edge: "17",
firefox: "50", firefox: "50",
chrome: "60", chrome: "60",
safari: "11.1", safari: "11.1",
ie: "11", ie: "11",
}, },
useBuiltIns: "usage", useBuiltIns: "usage",
corejs: 3 corejs: 3
} }
] ]
] ]
}, },
test: /\.js$/i, test: /\.js$/i,
exclude: /node_modules/, exclude: /node_modules/,
}, },
{ {
test: /\.css$/i, test: /\.css$/i,
use: [ MiniCssExtractPlugin.loader, use: [ MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { sourceMap: true }}, { loader: 'css-loader', options: { sourceMap: true }},
{ loader: 'postcss-loader', options: { { loader: 'postcss-loader', options: {
sourceMap: true, sourceMap: true,
plugins: () => [ postcssPresetEnv ] plugins: () => [ postcssPresetEnv ]
}}, }},
{ loader: 'resolve-url-loader', options: { sourceMap: true }} { loader: 'resolve-url-loader', options: { sourceMap: true }}
] ]
}, },
{ {
test: /\.s(c|a)ss$/i, test: /\.s(c|a)ss$/i,
use: [ MiniCssExtractPlugin.loader, use: [ MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { sourceMap: true }}, { loader: 'css-loader', options: { sourceMap: true }},
{ loader: 'postcss-loader', options: { { loader: 'postcss-loader', options: {
sourceMap: true, sourceMap: true,
plugins: () => [ postcssPresetEnv ] plugins: () => [ postcssPresetEnv ]
}}, }},
{ loader: 'resolve-url-loader', options: { sourceMap: true }}, { loader: 'resolve-url-loader', options: { sourceMap: true }},
{ loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: true }} { loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: true }}
] ]
}, },
{ {
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/i, test: /\.(woff(2)?|ttf|eot|svg)(\?.*)?$/i,
use: [ use: [
{ {
loader: 'file-loader', loader: 'file-loader',
@ -103,7 +111,8 @@ module.exports = {
if (chunk.name) { if (chunk.name) {
return chunk.name; return chunk.name;
} }
return chunk.modules.map(m => path.relative(m.context, m.request)).join("_"); let modules = chunk.modules || [chunk.entryModule];
return modules.map(m => path.relative(m.context, m.request)).join("_");
}), }),
new webpack.NamedModulesPlugin(), new webpack.NamedModulesPlugin(),
new ManifestPlugin({ new ManifestPlugin({
@ -113,7 +122,9 @@ module.exports = {
serialize: yaml.safeDump serialize: yaml.safeDump
}), }),
new CleanWebpackPlugin({ new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: path.resolve(__dirname, 'static', 'wp-*') cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, 'static'),
path.resolve(__dirname, 'well-known'),
]
}), }),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new CopyPlugin([ new CopyPlugin([
@ -121,7 +132,65 @@ module.exports = {
]), ]),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
VERSION: JSON.stringify(packageVersion) VERSION: JSON.stringify(packageVersion)
}) }),
...(() => {
const faviconJson = require('./config/favicon.json');
const langs = new Set();
function findLangs(json) {
if (json && json._i18n) {
Object.keys(json).forEach(key => {
if (key !== '_i18n') {
langs.add(key);
}
})
} else if (Array.isArray(json)) {
json.forEach(elem => findLangs(elem));
} else if (typeof json === 'object') {
Object.keys(json).forEach(key => findLangs(json[key]));
}
}
findLangs(faviconJson);
function selectLang(lang, json) {
if (json && json._i18n) {
return json[lang];
} else if (Array.isArray(json)) {
return json.map(elem => selectLang(lang, elem));
} else if (typeof json === 'object') {
return Object.fromEntries(Object.entries(json).map(([k, v]) => [k, selectLang(lang, v)]));
} else {
return json;
}
}
const langJsons = {};
Array.from(langs).forEach(lang => {
langJsons[lang] = selectLang(lang, faviconJson);
});
return Array.from(langs).map(lang => {
const tmpobj = tmp.fileSync({ dir: ".", postfix: ".json" });
fs.writeSync(tmpobj.fd, JSON.stringify(langJsons[lang]));
fs.close(tmpobj.fd);
return [
new RealFaviconPlugin({
faviconJson: `./${tmpobj.name}`,
outputPath: path.resolve(__dirname, 'well-known', lang),
inject: false
}),
new CopyPlugin([
{ from: 'config/robots.txt', to: path.resolve(__dirname, 'well-known', lang, 'robots.txt') },
])
];
}).flat(1);
})(),
{ apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => {
const imgFiles = glob.sync(path.resolve(__dirname, 'well-known', '**', '*.@(png)'));
const imgFilesArgs = Array.from(imgFiles).join(" ");
execSync(`exiftool -overwrite_original -all= ${imgFilesArgs}`, { stdio: 'inherit' });
})
}
], ],
output: { output: {
@ -142,7 +211,7 @@ module.exports = {
sourceMap: true sourceMap: true
}), }),
new OptimizeCSSAssetsPlugin({ new OptimizeCSSAssetsPlugin({
cssProcessorOptions: { cssProcessorOptions: {
map: { map: {
inline: false inline: false
} }