Merge branch 'master' into info-lecturer
This commit is contained in:
commit
2205180350
143
CHANGELOG.md
143
CHANGELOG.md
@ -2,6 +2,149 @@
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
### [7.3.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.3.1...v7.3.2) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **exam-users:** make csv import much more lenient ([2ddb566](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2ddb566))
|
||||||
|
* **mail:** honor userCsvOptions and userDisplayEmail ([89adf7f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/89adf7f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### [7.3.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.3.0...v7.3.1) (2019-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **course-edit:** edit courses without being school-wide lecturer ([d7d1f27](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d7d1f27)), closes [#464](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/464)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [7.3.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.2...v7.3.0) (2019-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **course-application:** better display of priorities ([64f7715](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/64f7715))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **csv:** allow customisation of csv-export-options ([95ceedd](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/95ceedd))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### [7.2.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.1...v7.2.2) (2019-09-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **authorisation:** keep showing allocations (ro) to lecturers ([c8e1d51](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c8e1d51))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### [7.2.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.2.0...v7.2.1) (2019-09-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix build ([69f4a80](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/69f4a80))
|
||||||
|
* fix tutorial registration group applying globally ([d2ba173](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d2ba173))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [7.2.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.2...v7.2.0) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* bump changelog ([60a7bb2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/60a7bb2))
|
||||||
|
* don't treat ExamBonusManual as override ([16abcd2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/16abcd2))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **course-applications:** automatic acceptance of direct applicants ([620950d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/620950d))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### [7.1.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.1...v7.1.2) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **exams:** include bonus points in sum for exam participants ([2bc6894](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/2bc6894))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### [7.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.1.0...v7.1.1) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix build ([d13ace4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d13ace4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [7.1.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v7.0.0...v7.1.0) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **datepicker:** select time from preselected date on edit ([d3375bb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d3375bb))
|
||||||
|
* **jobs:** cleaner shutdown of job-pool-manager ([adc8d46](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/adc8d46))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **exams:** re-introduce ExamBonusManual ([54e94a6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/54e94a6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [7.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.1...v7.0.0) (2019-09-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix startup on unix-socket ([39f1295](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/39f1295))
|
||||||
|
* improve async behaviour ([cc7a528](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/cc7a528))
|
||||||
|
* make migration idempotent again ([9778404](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9778404))
|
||||||
|
* restore behaviour of waiting asynchronously for job-management ([5ebcd89](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5ebcd89))
|
||||||
|
* **communication:** make communication form more intuitive ([7a2b972](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7a2b972)), closes [#387](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/387)
|
||||||
|
* fix migration ([d2478a3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d2478a3))
|
||||||
|
* fix migration & tests ([e05ea8e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e05ea8e))
|
||||||
|
* migration ([4383eb1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/4383eb1))
|
||||||
|
* syntax ([7afd569](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7afd569))
|
||||||
|
* **migration:** drop more tables in w.a. for inconsistent 21→22 ([d79dca6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d79dca6))
|
||||||
|
* typo ([fb1e42d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fb1e42d))
|
||||||
|
|
||||||
|
|
||||||
|
### chore
|
||||||
|
|
||||||
|
* bump versions ([67e3b38](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/67e3b38))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **course:** additional crosslinking ([5eaba78](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5eaba78))
|
||||||
|
* **exam-users:** document part-* family of columns ([fe07a22](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fe07a22))
|
||||||
|
* **exams:** accept/reset computed results ([72342f1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/72342f1))
|
||||||
|
* **exams:** automatically compute examResults ([ea5a398](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ea5a398))
|
||||||
|
* **exams:** better display exam-result-information ([0ebda4d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0ebda4d))
|
||||||
|
* **exams:** csv-import of ExamPartResults ([29f4e28](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/29f4e28))
|
||||||
|
* **exams:** implement rounding of exambonus ([e97cd56](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e97cd56))
|
||||||
|
* **exams:** refine exam form ([014a17a](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/014a17a))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* yesod >=1.6
|
||||||
|
* **exams:** examPartName no longer required
|
||||||
|
* **exams:** Introduces ExamPartNumbers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### [6.11.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.0...v6.11.1) (2019-09-17)
|
### [6.11.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v6.11.0...v6.11.1) (2019-09-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -67,7 +67,7 @@ update = do
|
|||||||
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
|
restartAppInNewThread tidStore = modifyStoredIORef tidStore $ \tid -> do
|
||||||
killThread tid
|
killThread tid
|
||||||
withStore doneStore takeMVar
|
withStore doneStore takeMVar
|
||||||
readStore doneStore >>= start
|
withStore doneStore start
|
||||||
|
|
||||||
|
|
||||||
-- | Start the server in a separate thread.
|
-- | Start the server in a separate thread.
|
||||||
@ -77,10 +77,7 @@ update = do
|
|||||||
(port, site, app) <- getApplicationRepl
|
(port, site, app) <- getApplicationRepl
|
||||||
resourceForkIO $ do
|
resourceForkIO $ do
|
||||||
finally (liftIO $ runSettings (setPort port defaultSettings) app)
|
finally (liftIO $ runSettings (setPort port defaultSettings) app)
|
||||||
-- Note that this implies concurrency
|
(liftIO $ shutdownApp site `finally` putMVar done ())
|
||||||
-- between shutdownApp and the next app that is starting.
|
|
||||||
-- Normally this should be fine
|
|
||||||
(liftIO $ putMVar done () >> shutdownApp site)
|
|
||||||
|
|
||||||
-- | kill the server
|
-- | kill the server
|
||||||
shutdown :: IO ()
|
shutdown :: IO ()
|
||||||
|
|||||||
@ -8,3 +8,5 @@ log-settings:
|
|||||||
destination: "test.log"
|
destination: "test.log"
|
||||||
|
|
||||||
auth-dummy-login: true
|
auth-dummy-login: true
|
||||||
|
|
||||||
|
job-workers: 1
|
||||||
|
|||||||
@ -24,17 +24,6 @@ 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 and a format string and parses the given date to a Date object.
|
|
||||||
* If the date string is not valid (i.e. cannot be parsed with the given format string), returns undefined.
|
|
||||||
* @param {*} dateStr string representation of a date
|
|
||||||
* @param {*} dateFormat format string of the date
|
|
||||||
*/
|
|
||||||
function parseDateWithFormat(dateStr, dateFormat) {
|
|
||||||
const parsedMomentDate = moment(dateStr, dateFormat);
|
|
||||||
if (parsedMomentDate.isValid()) return parsedMomentDate.toDate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string.
|
* 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;
|
* If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string;
|
||||||
@ -136,6 +125,9 @@ export class Datepicker {
|
|||||||
throw new Error('Datepicker utility called on unsupported element!');
|
throw new Error('Datepicker utility called on unsupported element!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// format any existing dates to fancy display format on pageload
|
||||||
|
this.formatElementValue(true);
|
||||||
|
|
||||||
// initialize tail.datetime (datepicker) instance
|
// initialize tail.datetime (datepicker) instance
|
||||||
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
|
this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig });
|
||||||
|
|
||||||
@ -187,9 +179,6 @@ export class Datepicker {
|
|||||||
|
|
||||||
// format the date value of the form input element of this datepicker before form submission
|
// format the date value of the form input element of this datepicker before form submission
|
||||||
this._element.form.addEventListener('submit', () => this.formatElementValue());
|
this._element.form.addEventListener('submit', () => this.formatElementValue());
|
||||||
|
|
||||||
// format any existing dates to fancy display format on pageload
|
|
||||||
this.formatElementValue(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
@ -201,22 +190,21 @@ export class Datepicker {
|
|||||||
* @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.
|
||||||
*/
|
*/
|
||||||
formatElementValue(toFancy) {
|
formatElementValue(toFancy) {
|
||||||
const dp = this.datepickerInstance;
|
|
||||||
if (this._element.value) {
|
if (this._element.value) {
|
||||||
if (toFancy) {
|
this._element.value = this.unformat(toFancy);
|
||||||
const parsedDate = parseDateWithFormat(this._element.value, FORM_DATE_FORMAT[this.elementType]);
|
|
||||||
if (parsedDate) dp.selectDate();
|
|
||||||
} else {
|
|
||||||
this._element.value = this.unformat();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
unformat() {
|
unformat(toFancy) {
|
||||||
return reformatDateString(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType], FORM_DATE_FORMAT[this.elementType]);
|
const formatIn = toFancy ? FORM_DATE_FORMAT[this.elementType] : FORM_DATE_FORMAT_MOMENT[this.elementType];
|
||||||
|
const formatOut = toFancy ? FORM_DATE_FORMAT_MOMENT[this.elementType] : FORM_DATE_FORMAT[this.elementType];
|
||||||
|
return reformatDateString(this._element.value, formatIn, formatOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,4 +15,4 @@ if [[ -d .stack-work-doc ]]; then
|
|||||||
trap move-back EXIT
|
trap move-back EXIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
stack build --fast --flag uniworx:library-only --flag uniworx:dev --haddock --haddock-hyperlink-source --haddock-deps --haddock-internal
|
stack build --fast --flag uniworx:library-only --flag uniworx:dev --haddock --haddock-hyperlink-source --haddock-deps --haddock-internal ${@}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
[[ -n "${FORCE_RELEASE}" ]] && exit 0
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
|||||||
@ -173,6 +173,7 @@ CourseApplicationTemplateApplication: Bewerbungsvorlage(n)
|
|||||||
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
|
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
|
||||||
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
|
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
|
||||||
CourseApplication: Bewerbung
|
CourseApplication: Bewerbung
|
||||||
|
CourseApplicationIsParticipant: Kursteilnehmer
|
||||||
|
|
||||||
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
|
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
|
||||||
CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden
|
CourseApplicationInvalidAction: Angegeben Aktion kann nicht durchgeführt werden
|
||||||
@ -1135,10 +1136,14 @@ NavigationFavourites: Favoriten
|
|||||||
|
|
||||||
CommSubject: Betreff
|
CommSubject: Betreff
|
||||||
CommBody: Nachricht
|
CommBody: Nachricht
|
||||||
|
CommBodyTip: Das Eingabefeld akzeptiert derzeit ausschließlich Html. U.A. Zeilumbrüche werden dementsprechend ignoriert und müssen manuell mit <br> eingefügt werden.
|
||||||
CommRecipients: Empfänger
|
CommRecipients: Empfänger
|
||||||
CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
|
CommRecipientsTip: Sie selbst erhalten immer eine Kopie der Nachricht
|
||||||
|
CommRecipientsList: Die an Sie selbst verschickte Kopie der Nachricht wird, zu Archivierungszwecken, eine vollständige Liste aller Empfänger enthalten. Die Empfängerliste wird im CSV-Format and die E-Mail angehängt. Andere Empfänger erhalten die Liste nicht. Bitte entfernen Sie dementsprechend den Anhang bevor Sie die E-Mail weiterleiten oder anderweitig mit Dritten teilen.
|
||||||
CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert
|
CommDuplicateRecipients n@Int: #{n} #{pluralDE n "doppelter" "doppelte"} Empfänger ignoriert
|
||||||
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
CommSuccess n@Int: Nachricht wurde an #{n} Empfänger versandt
|
||||||
|
CommUndisclosedRecipients: Verborgene Empfänger
|
||||||
|
CommAllRecipients: alle-empfaenger
|
||||||
|
|
||||||
CommCourseHeading: Kursmitteilung
|
CommCourseHeading: Kursmitteilung
|
||||||
CommTutorialHeading: Tutorium-Mitteilung
|
CommTutorialHeading: Tutorium-Mitteilung
|
||||||
@ -1346,6 +1351,7 @@ ExamBonus: Bonuspunkte-System
|
|||||||
ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
|
ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
|
||||||
ExamNoBonus': Kein automatischer Bonus
|
ExamNoBonus': Kein automatischer Bonus
|
||||||
ExamBonusPoints': Umrechnung von Übungspunkten
|
ExamBonusPoints': Umrechnung von Übungspunkten
|
||||||
|
ExamBonusManual': Manuelle Berechnung
|
||||||
|
|
||||||
ExamBonusAchieved: Bonuspunkte
|
ExamBonusAchieved: Bonuspunkte
|
||||||
|
|
||||||
@ -1415,6 +1421,7 @@ ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
|
|||||||
ExamNoShow: Nicht erschienen
|
ExamNoShow: Nicht erschienen
|
||||||
ExamVoided: Entwertet
|
ExamVoided: Entwertet
|
||||||
|
|
||||||
|
ExamBonusManualParticipants: Von den Kursverwaltern manuell berechnet
|
||||||
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte
|
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte
|
||||||
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist
|
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist
|
||||||
|
|
||||||
@ -1494,8 +1501,8 @@ Proportion c@Text of@Text prop@Rational: #{c}/#{of} (#{rationalToFixed2 (100 * p
|
|||||||
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
|
||||||
CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
|
CourseApplicationsTableCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungen
|
||||||
|
|
||||||
CsvColumnsExplanationsLabel: Spalten
|
CsvColumnsExplanationsLabel: Spalten- & Zellenformat
|
||||||
CsvColumnsExplanationsTip: Bedeutung der in der CSV-Datei enthaltenen Spalten
|
CsvColumnsExplanationsTip: Bedeutung und Format der in der CSV-Datei enthaltenen Spalten
|
||||||
CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
|
CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
|
||||||
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
|
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
|
||||||
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
|
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
|
||||||
@ -1525,7 +1532,7 @@ CsvColumnApplicationsSemester: Fachsemester des Bewerbes im assoziierten Studien
|
|||||||
CsvColumnApplicationsText: Text-Bewerbung
|
CsvColumnApplicationsText: Text-Bewerbung
|
||||||
CsvColumnApplicationsHasFiles: Hat der Bewerber Dateien zu seiner Bewerbung eingereicht (siehe ZIP-Archiv aller Bewerbungsdateien)?
|
CsvColumnApplicationsHasFiles: Hat der Bewerber Dateien zu seiner Bewerbung eingereicht (siehe ZIP-Archiv aller Bewerbungsdateien)?
|
||||||
CsvColumnApplicationsVeto: Bewerber mit Veto werden garantiert nicht dem Kurs zugeteilt; "veto" oder leer
|
CsvColumnApplicationsVeto: Bewerber mit Veto werden garantiert nicht dem Kurs zugeteilt; "veto" oder leer
|
||||||
CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0"
|
CsvColumnApplicationsRating: Bewertung der Bewerbung; "1.0", "1.3", "1.7", ..., "4.0", "5.0" (Leer wird behandelt wie eine Note zwischen 2.3 und 2.7)
|
||||||
CsvColumnApplicationsComment: Kommentar zur Bewerbung; je nach Kurs-Einstellungen entweder nur als Notiz für die Kursverwalter oder Feedback für den Bewerber
|
CsvColumnApplicationsComment: Kommentar zur Bewerbung; je nach Kurs-Einstellungen entweder nur als Notiz für die Kursverwalter oder Feedback für den Bewerber
|
||||||
|
|
||||||
Action: Aktion
|
Action: Aktion
|
||||||
@ -1785,3 +1792,38 @@ ExamClosedSince time@Text: Klausur abgeschlossen seit #{time}
|
|||||||
|
|
||||||
LecturerInfoTooltipNew: Neues Feature
|
LecturerInfoTooltipNew: Neues Feature
|
||||||
LecturerInfoTooltipProblem: Noch nicht implementiertes Feature oder Feature mit bekannten Problemen
|
LecturerInfoTooltipProblem: Noch nicht implementiertes Feature oder Feature mit bekannten Problemen
|
||||||
|
|
||||||
|
BtnAcceptApplications: Bewerbungen akzeptieren
|
||||||
|
BtnAcceptApplicationsTip: Mit dem untigen Knopf können Sie den Kurs (höchstens bis zur angegeben Maximalkapazität, falls eingestellt) mit Bewerbern auffüllen. Die Bewertungen der Bewerbungen werden dabei berücksichtigt (Unbewertet wird behandelt wie eine Note zwischen 2.3 und 2.7). Bewerber mit Veto oder 5.0 werden nicht angemeldet.
|
||||||
|
AcceptApplicationsMode: Bewerbungen akzeptieren
|
||||||
|
AcceptApplicationsModeTip: Sollen akzeptierte Bewerber direkt als Teilnehmer im Kurs eingetragen werden oder sollen Einladungen per E-Mail verschickt werden?
|
||||||
|
AcceptApplicationsDirect: Direkt anmelden
|
||||||
|
AcceptApplicationsInvite: Einladungen verschicken
|
||||||
|
AcceptApplicationsSecondary: Gleichstände auflösen
|
||||||
|
AcceptApplicationsSecondaryTip: Wenn es im Laufe des Verfahrens mehrere Bewerber mit der selben Bewertung für den selben Platz gibt, wie soll der Gleichstand aufgelöst werden?
|
||||||
|
AcceptApplicationsSecondaryRandom: Zufällig
|
||||||
|
AcceptApplicationsSecondaryTime: Nach Zeitpunkt der Bewerbung
|
||||||
|
|
||||||
|
CsvOptions: CSV-Optionen
|
||||||
|
CsvOptionsTip: Diese Einstellungen betreffen nur den CSV-Export; beim Import werden die verwendeten Einstellungen automatisch ermittelt.
|
||||||
|
CsvPresetRFC: Standard-Konform (RFC 4180)
|
||||||
|
CsvPresetExcel: Excel-Kompatibel
|
||||||
|
CsvCustom: Benutzerdefiniert
|
||||||
|
CsvDelimiter: Trennzeichen
|
||||||
|
CsvUseCrLf: Zeilenumbrüche
|
||||||
|
CsvQuoting: Quoting
|
||||||
|
CsvQuotingTip: Wann sollen Anführungszeichen (") um Felder platziert werden, um Interpretation von im Feld enthaltenen Zeichen als Trennzeichen zu verhindern?
|
||||||
|
CsvDelimiterNull: Null-Byte
|
||||||
|
CsvDelimiterTab: Tabulator
|
||||||
|
CsvDelimiterComma: Komma
|
||||||
|
CsvDelimiterColon: Doppelpunkt
|
||||||
|
CsvDelimiterBar: Senkrechter Strich
|
||||||
|
CsvDelimiterSpace: Leerzeichen
|
||||||
|
CsvDelimiterUnitSep: Teilgruppentrennzeichen
|
||||||
|
CsvCrLf: DOS (CRLF)
|
||||||
|
CsvLf: Unix (LF)
|
||||||
|
CsvQuoteNone: Nie
|
||||||
|
CsvQuoteMinimal: Nur wenn nötig
|
||||||
|
CsvQuoteAll: Immer
|
||||||
|
CsvOptionsUpdated: CSV-Optionen erfolgreich angepasst
|
||||||
|
CsvChangeOptionsLabel: Export-Optionen
|
||||||
|
|||||||
@ -20,7 +20,7 @@ Course -- Information about a single course; contained info is always visible
|
|||||||
applicationsRequired Bool default=false
|
applicationsRequired Bool default=false
|
||||||
applicationsInstructions Html Maybe
|
applicationsInstructions Html Maybe
|
||||||
applicationsText Bool default=false
|
applicationsText Bool default=false
|
||||||
applicationsFiles UploadMode "default='{ \"mode\": \"no-upload\" }'::jsonb"
|
applicationsFiles UploadMode "default='{\"mode\": \"no-upload\"}'::jsonb"
|
||||||
applicationsRatingsVisible Bool default=false
|
applicationsRatingsVisible Bool default=false
|
||||||
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
|
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
|
||||||
TermSchoolCourseName term school name -- name must be unique within school and semester
|
TermSchoolCourseName term school name -- name must be unique within school and semester
|
||||||
|
|||||||
@ -30,6 +30,7 @@ User json -- Each Uni2work user has a corresponding row in this table; create
|
|||||||
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined
|
mailLanguages MailLanguages "default='[]'::jsonb" -- Preferred language for eMail; i18n not yet implemented; user-defined
|
||||||
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
|
notificationSettings NotificationSettings -- Bit-array for which events email notifications are requested by user; user-defined
|
||||||
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
|
warningDays NominalDiffTime default=1209600 -- timedistance to pending deadlines for homepage infos
|
||||||
|
csvOptions CsvOptions "default='{}'::jsonb"
|
||||||
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
|
UniqueAuthentication ident -- Column 'ident' can be used as a row-key in this table
|
||||||
UniqueEmail email -- Column 'email' can be used as a row-key in this table
|
UniqueEmail email -- Column 'email' can be used as a row-key in this table
|
||||||
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
deriving Show Eq Ord Generic -- Haskell-specific settings for runtime-value representing a row in memory
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "6.11.1",
|
"version": "7.3.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "6.11.1",
|
"version": "7.3.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: uniworx
|
name: uniworx
|
||||||
version: 6.11.1
|
version: 7.3.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- base >=4.9.1.0 && <5
|
- base >=4.9.1.0 && <5
|
||||||
|
|||||||
1
routes
1
routes
@ -72,6 +72,7 @@
|
|||||||
/user/profile ProfileDataR GET !free
|
/user/profile ProfileDataR GET !free
|
||||||
/user/authpreds AuthPredsR GET POST !free
|
/user/authpreds AuthPredsR GET POST !free
|
||||||
/user/set-display-email SetDisplayEmailR GET POST !free
|
/user/set-display-email SetDisplayEmailR GET POST !free
|
||||||
|
/user/csv-options CsvOptionsR GET POST !free
|
||||||
|
|
||||||
/exam-office ExamOfficeR !exam-office:
|
/exam-office ExamOfficeR !exam-office:
|
||||||
/ EOExamsR GET
|
/ EOExamsR GET
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import Language.Haskell.TH.Syntax (qLocation)
|
|||||||
import Network.Wai (Middleware)
|
import Network.Wai (Middleware)
|
||||||
import Network.Wai.Handler.Warp (Settings, defaultSettings,
|
import Network.Wai.Handler.Warp (Settings, defaultSettings,
|
||||||
defaultShouldDisplayException,
|
defaultShouldDisplayException,
|
||||||
runSettingsSocket, setHost,
|
runSettings, runSettingsSocket, setHost,
|
||||||
setBeforeMainLoop,
|
setBeforeMainLoop,
|
||||||
setOnException, setPort, getPort)
|
setOnException, setPort, getPort)
|
||||||
import Data.Streaming.Network (bindPortTCP)
|
import Data.Streaming.Network (bindPortTCP)
|
||||||
@ -74,14 +74,15 @@ import qualified Database.Memcached.Binary.IO as Memcached
|
|||||||
import qualified System.Systemd.Daemon as Systemd
|
import qualified System.Systemd.Daemon as Systemd
|
||||||
import System.Environment (lookupEnv)
|
import System.Environment (lookupEnv)
|
||||||
import System.Posix.Process (getProcessID)
|
import System.Posix.Process (getProcessID)
|
||||||
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM)
|
import System.Posix.Signals (SignalInfo(..), installHandler, sigTERM, sigINT)
|
||||||
import qualified System.Posix.Signals as Signals (Handler(..))
|
import qualified System.Posix.Signals as Signals (Handler(..))
|
||||||
|
|
||||||
import Network.Socket (socketPort)
|
import Network.Socket (socketPort, Socket, PortNumber)
|
||||||
import qualified Network.Socket as Socket (close)
|
import qualified Network.Socket as Socket (close)
|
||||||
|
|
||||||
import Control.Concurrent.STM.Delay
|
import Control.Concurrent.STM.Delay
|
||||||
import Control.Monad.STM (retry)
|
import Control.Monad.STM (retry)
|
||||||
|
import Control.Monad.Trans.Cont (runContT, callCC)
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
@ -366,11 +367,20 @@ develMain = runResourceT $ do
|
|||||||
wsettings <- liftIO . getDevSettings $ warpSettings foundation
|
wsettings <- liftIO . getDevSettings $ warpSettings foundation
|
||||||
app <- makeApplication foundation
|
app <- makeApplication foundation
|
||||||
|
|
||||||
|
let
|
||||||
|
awaitTermination :: IO ()
|
||||||
|
awaitTermination
|
||||||
|
= flip runContT return . forever $ do
|
||||||
|
lift $ threadDelay 100e3
|
||||||
|
whenM (lift $ doesFileExist "yesod-devel/devel-terminate") $
|
||||||
|
callCC ($ ())
|
||||||
|
|
||||||
|
void . liftIO $ installHandler sigINT (Signals.Catch $ return ()) Nothing
|
||||||
runAppLoggingT foundation $ handleJobs foundation
|
runAppLoggingT foundation $ handleJobs foundation
|
||||||
liftIO . develMainHelper $ return (wsettings, app)
|
void . liftIO $ awaitTermination `race` runSettings wsettings app
|
||||||
|
|
||||||
-- | The @main@ function for an executable running this site.
|
-- | The @main@ function for an executable running this site.
|
||||||
appMain :: MonadUnliftIO m => m ()
|
appMain :: forall m. (MonadUnliftIO m, MonadMask m) => m ()
|
||||||
appMain = runResourceT $ do
|
appMain = runResourceT $ do
|
||||||
settings <- getAppSettings
|
settings <- getAppSettings
|
||||||
|
|
||||||
@ -398,7 +408,7 @@ appMain = runResourceT $ do
|
|||||||
$logInfoS "bind" [st|Listening on #{tshow host} port #{tshow port} as per configuration|]
|
$logInfoS "bind" [st|Listening on #{tshow host} port #{tshow port} as per configuration|]
|
||||||
liftIO $ pure <$> bindPortTCP port host
|
liftIO $ pure <$> bindPortTCP port host
|
||||||
|
|
||||||
$logDebugS "bind" . tshow =<< mapM (liftIO . socketPort) sockets
|
$logDebugS "bind" . tshow =<< mapM (liftIO . try . socketPort :: Socket -> _ (Either SomeException PortNumber)) sockets
|
||||||
|
|
||||||
mainThreadId <- myThreadId
|
mainThreadId <- myThreadId
|
||||||
liftIO . void . flip (installHandler sigTERM) Nothing . Signals.CatchInfo $ \SignalInfo{..} -> runAppLoggingT foundation $ do
|
liftIO . void . flip (installHandler sigTERM) Nothing . Signals.CatchInfo $ \SignalInfo{..} -> runAppLoggingT foundation $ do
|
||||||
@ -462,7 +472,7 @@ appMain = runResourceT $ do
|
|||||||
foundationStoreNum :: Word32
|
foundationStoreNum :: Word32
|
||||||
foundationStoreNum = 2
|
foundationStoreNum = 2
|
||||||
|
|
||||||
getApplicationRepl :: (MonadResource m, MonadUnliftIO m) => m (Int, UniWorX, Application)
|
getApplicationRepl :: (MonadResource m, MonadUnliftIO m, MonadMask m) => m (Int, UniWorX, Application)
|
||||||
getApplicationRepl = do
|
getApplicationRepl = do
|
||||||
settings <- getAppDevSettings
|
settings <- getAppDevSettings
|
||||||
foundation <- makeFoundation settings
|
foundation <- makeFoundation settings
|
||||||
|
|||||||
@ -315,6 +315,8 @@ embedRenderMessage ''UniWorX ''UploadModeDescr id
|
|||||||
embedRenderMessage ''UniWorX ''SecretJSONFieldException id
|
embedRenderMessage ''UniWorX ''SecretJSONFieldException id
|
||||||
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
|
embedRenderMessage ''UniWorX ''AFormMessage $ concat . drop 2 . splitCamel
|
||||||
embedRenderMessage ''UniWorX ''SchoolFunction id
|
embedRenderMessage ''UniWorX ''SchoolFunction id
|
||||||
|
embedRenderMessage ''UniWorX ''CsvPreset id
|
||||||
|
embedRenderMessage ''UniWorX ''Quoting ("Csv" <>)
|
||||||
|
|
||||||
embedRenderMessage ''UniWorX ''AuthenticationMode id
|
embedRenderMessage ''UniWorX ''AuthenticationMode id
|
||||||
|
|
||||||
@ -933,7 +935,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
|
|||||||
return Authorized
|
return Authorized
|
||||||
|
|
||||||
r -> $unsupportedAuthPredicate AuthTime r
|
r -> $unsupportedAuthPredicate AuthTime r
|
||||||
tagAccessPredicate AuthStaffTime = APDB $ \_ route _ -> case route of
|
tagAccessPredicate AuthStaffTime = APDB $ \_ route isWrite -> case route of
|
||||||
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
|
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
|
||||||
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
|
allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
|
||||||
@ -944,7 +946,8 @@ tagAccessPredicate AuthStaffTime = APDB $ \_ route _ -> case route of
|
|||||||
Just Allocation{..} -> do
|
Just Allocation{..} -> do
|
||||||
cTime <- liftIO getCurrentTime
|
cTime <- liftIO getCurrentTime
|
||||||
guard $ NTop allocationStaffAllocationFrom <= NTop (Just cTime)
|
guard $ NTop allocationStaffAllocationFrom <= NTop (Just cTime)
|
||||||
guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo
|
when isWrite $
|
||||||
|
guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo
|
||||||
|
|
||||||
return Authorized
|
return Authorized
|
||||||
|
|
||||||
@ -1197,10 +1200,11 @@ tagAccessPredicate AuthRegisterGroup = APDB $ \mAuthId route _ -> case route of
|
|||||||
(Nothing, _) -> return Authorized
|
(Nothing, _) -> return Authorized
|
||||||
(_, Nothing) -> return AuthenticationRequired
|
(_, Nothing) -> return AuthenticationRequired
|
||||||
(Just rGroup, Just uid) -> do
|
(Just rGroup, Just uid) -> do
|
||||||
[E.Value hasOther] <- $cachedHereBinary (uid, rGroup) . lift . E.select . return . E.exists . E.from $ \(tutorial `E.InnerJoin` participant) -> do
|
hasOther <- $cachedHereBinary (uid, rGroup) . lift . E.selectExists . E.from $ \(tutorial `E.InnerJoin` participant) -> do
|
||||||
E.on $ tutorial E.^. TutorialId E.==. participant E.^. TutorialParticipantTutorial
|
E.on $ tutorial E.^. TutorialId E.==. participant E.^. TutorialParticipantTutorial
|
||||||
E.where_ $ participant E.^. TutorialParticipantUser E.==. E.val uid
|
E.&&. tutorial E.^. TutorialCourse E.==. E.val tutorialCourse
|
||||||
E.&&. tutorial E.^. TutorialRegGroup E.==. E.just (E.val rGroup)
|
E.&&. tutorial E.^. TutorialRegGroup E.==. E.just (E.val rGroup)
|
||||||
|
E.&&. participant E.^. TutorialParticipantUser E.==. E.val uid
|
||||||
guard $ not hasOther
|
guard $ not hasOther
|
||||||
return Authorized
|
return Authorized
|
||||||
r -> $unsupportedAuthPredicate AuthRegisterGroup r
|
r -> $unsupportedAuthPredicate AuthRegisterGroup r
|
||||||
@ -3285,6 +3289,7 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
, userWarningDays = userDefaultWarningDays
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userMailLanguages = def
|
, userMailLanguages = def
|
||||||
|
, userCsvOptions = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Just now
|
, userLastLdapSynchronisation = Just now
|
||||||
|
|||||||
@ -82,8 +82,7 @@ applicationForm maId@(is _Just -> isAlloc) cid uid ApplicationFormMode{..} csrf
|
|||||||
coursesNum <- fromIntegral . fromMaybe 1 <$> for maId (\aId -> count [AllocationCourseAllocation ==. aId])
|
coursesNum <- fromIntegral . fromMaybe 1 <$> for maId (\aId -> count [AllocationCourseAllocation ==. aId])
|
||||||
course <- getJust cid
|
course <- getJust cid
|
||||||
(fromMaybe 0 -> maxPrio) <- fmap ((>>= E.unValue) . listToMaybe) . E.select . E.from $ \courseApplication -> do
|
(fromMaybe 0 -> maxPrio) <- fmap ((>>= E.unValue) . listToMaybe) . E.select . E.from $ \courseApplication -> do
|
||||||
E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid
|
E.where_ $ courseApplication E.^. CourseApplicationUser E.==. E.val uid
|
||||||
E.&&. courseApplication E.^. CourseApplicationUser E.==. E.val uid
|
|
||||||
E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.val maId
|
E.&&. courseApplication E.^. CourseApplicationAllocation E.==. E.val maId
|
||||||
E.&&. E.not_ (E.isNothing $ courseApplication E.^. CourseApplicationAllocationPriority)
|
E.&&. E.not_ (E.isNothing $ courseApplication E.^. CourseApplicationAllocationPriority)
|
||||||
return . E.joinV . E.max_ $ courseApplication E.^. CourseApplicationAllocationPriority
|
return . E.joinV . E.max_ $ courseApplication E.^. CourseApplicationAllocationPriority
|
||||||
|
|||||||
@ -25,6 +25,10 @@ import qualified Data.Map as Map
|
|||||||
|
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
import Handler.Course.ParticipantInvite
|
||||||
|
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
|
||||||
type CourseApplicationsTableExpr = ( E.SqlExpr (Entity CourseApplication)
|
type CourseApplicationsTableExpr = ( E.SqlExpr (Entity CourseApplication)
|
||||||
`E.InnerJoin` E.SqlExpr (Entity User)
|
`E.InnerJoin` E.SqlExpr (Entity User)
|
||||||
@ -34,41 +38,49 @@ type CourseApplicationsTableExpr = ( E.SqlExpr (Entity CourseApplic
|
|||||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms))
|
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms))
|
||||||
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree))
|
`E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree))
|
||||||
)
|
)
|
||||||
|
`E.LeftOuterJoin` E.SqlExpr (Maybe (Entity CourseParticipant))
|
||||||
type CourseApplicationsTableData = DBRow ( Entity CourseApplication
|
type CourseApplicationsTableData = DBRow ( Entity CourseApplication
|
||||||
, Entity User
|
, Entity User
|
||||||
, E.Value Bool -- hasFiles
|
, Bool -- hasFiles
|
||||||
, Maybe (Entity Allocation)
|
, Maybe (Entity Allocation)
|
||||||
, Maybe (Entity StudyFeatures)
|
, Maybe (Entity StudyFeatures)
|
||||||
, Maybe (Entity StudyTerms)
|
, Maybe (Entity StudyTerms)
|
||||||
, Maybe (Entity StudyDegree)
|
, Maybe (Entity StudyDegree)
|
||||||
|
, Bool -- isParticipant
|
||||||
)
|
)
|
||||||
|
|
||||||
courseApplicationsIdent :: Text
|
courseApplicationsIdent :: Text
|
||||||
courseApplicationsIdent = "applications"
|
courseApplicationsIdent = "applications"
|
||||||
|
|
||||||
queryCourseApplication :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity CourseApplication))
|
queryCourseApplication :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity CourseApplication))
|
||||||
queryCourseApplication = to $ $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)
|
queryCourseApplication = to $ $(sqlIJproj 2 1) . $(sqlLOJproj 4 1)
|
||||||
|
|
||||||
queryUser :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity User))
|
queryUser :: Getter CourseApplicationsTableExpr (E.SqlExpr (Entity User))
|
||||||
queryUser = to $ $(sqlIJproj 2 2) . $(sqlLOJproj 3 1)
|
queryUser = to $ $(sqlIJproj 2 2) . $(sqlLOJproj 4 1)
|
||||||
|
|
||||||
queryHasFiles :: Getter CourseApplicationsTableExpr (E.SqlExpr (E.Value Bool))
|
queryHasFiles :: Getter CourseApplicationsTableExpr (E.SqlExpr (E.Value Bool))
|
||||||
queryHasFiles = to $ hasFiles . $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)
|
queryHasFiles = to $ hasFiles . $(sqlIJproj 2 1) . $(sqlLOJproj 4 1)
|
||||||
where
|
where
|
||||||
hasFiles appl = E.exists . E.from $ \courseApplicationFile ->
|
hasFiles appl = E.exists . E.from $ \courseApplicationFile ->
|
||||||
E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. appl E.^. CourseApplicationId
|
E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. appl E.^. CourseApplicationId
|
||||||
|
|
||||||
queryAllocation :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity Allocation)))
|
queryAllocation :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity Allocation)))
|
||||||
queryAllocation = to $(sqlLOJproj 3 2)
|
queryAllocation = to $(sqlLOJproj 4 2)
|
||||||
|
|
||||||
queryStudyFeatures :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyFeatures)))
|
queryStudyFeatures :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyFeatures)))
|
||||||
queryStudyFeatures = to $ $(sqlIJproj 3 1) . $(sqlLOJproj 3 3)
|
queryStudyFeatures = to $ $(sqlIJproj 3 1) . $(sqlLOJproj 4 3)
|
||||||
|
|
||||||
queryStudyTerms :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyTerms)))
|
queryStudyTerms :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyTerms)))
|
||||||
queryStudyTerms = to $ $(sqlIJproj 3 2) . $(sqlLOJproj 3 3)
|
queryStudyTerms = to $ $(sqlIJproj 3 2) . $(sqlLOJproj 4 3)
|
||||||
|
|
||||||
queryStudyDegree :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyDegree)))
|
queryStudyDegree :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity StudyDegree)))
|
||||||
queryStudyDegree = to $ $(sqlIJproj 3 3) . $(sqlLOJproj 3 3)
|
queryStudyDegree = to $ $(sqlIJproj 3 3) . $(sqlLOJproj 4 3)
|
||||||
|
|
||||||
|
queryCourseParticipant :: Getter CourseApplicationsTableExpr (E.SqlExpr (Maybe (Entity CourseParticipant)))
|
||||||
|
queryCourseParticipant = to $(sqlLOJproj 4 4)
|
||||||
|
|
||||||
|
queryIsParticipant :: Getter CourseApplicationsTableExpr (E.SqlExpr (E.Value Bool))
|
||||||
|
queryIsParticipant = to $ E.not_ . E.isNothing . (E.?. CourseParticipantId) . $(sqlLOJproj 4 4)
|
||||||
|
|
||||||
resultCourseApplication :: Lens' CourseApplicationsTableData (Entity CourseApplication)
|
resultCourseApplication :: Lens' CourseApplicationsTableData (Entity CourseApplication)
|
||||||
resultCourseApplication = _dbrOutput . _1
|
resultCourseApplication = _dbrOutput . _1
|
||||||
@ -77,7 +89,7 @@ resultUser :: Lens' CourseApplicationsTableData (Entity User)
|
|||||||
resultUser = _dbrOutput . _2
|
resultUser = _dbrOutput . _2
|
||||||
|
|
||||||
resultHasFiles :: Lens' CourseApplicationsTableData Bool
|
resultHasFiles :: Lens' CourseApplicationsTableData Bool
|
||||||
resultHasFiles = _dbrOutput . _3 . _Value
|
resultHasFiles = _dbrOutput . _3
|
||||||
|
|
||||||
resultAllocation :: Traversal' CourseApplicationsTableData (Entity Allocation)
|
resultAllocation :: Traversal' CourseApplicationsTableData (Entity Allocation)
|
||||||
resultAllocation = _dbrOutput . _4 . _Just
|
resultAllocation = _dbrOutput . _4 . _Just
|
||||||
@ -91,6 +103,9 @@ resultStudyTerms = _dbrOutput . _6 . _Just
|
|||||||
resultStudyDegree :: Traversal' CourseApplicationsTableData (Entity StudyDegree)
|
resultStudyDegree :: Traversal' CourseApplicationsTableData (Entity StudyDegree)
|
||||||
resultStudyDegree = _dbrOutput . _7 . _Just
|
resultStudyDegree = _dbrOutput . _7 . _Just
|
||||||
|
|
||||||
|
resultIsParticipant :: Lens' CourseApplicationsTableData Bool
|
||||||
|
resultIsParticipant = _dbrOutput . _8
|
||||||
|
|
||||||
|
|
||||||
newtype CourseApplicationsTableVeto = CourseApplicationsTableVeto Bool
|
newtype CourseApplicationsTableVeto = CourseApplicationsTableVeto Bool
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
@ -207,10 +222,42 @@ instance Exception CourseApplicationsTableCsvException
|
|||||||
embedRenderMessage ''UniWorX ''CourseApplicationsTableCsvException id
|
embedRenderMessage ''UniWorX ''CourseApplicationsTableCsvException id
|
||||||
|
|
||||||
|
|
||||||
|
data ButtonAcceptApplications = BtnAcceptApplications
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ButtonAcceptApplications
|
||||||
|
instance Finite ButtonAcceptApplications
|
||||||
|
|
||||||
|
nullaryPathPiece ''ButtonAcceptApplications $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonAcceptApplications id
|
||||||
|
instance Button UniWorX ButtonAcceptApplications where
|
||||||
|
btnClasses BtnAcceptApplications = [BCIsButton]
|
||||||
|
|
||||||
|
data AcceptApplicationsMode = AcceptApplicationsInvite
|
||||||
|
| AcceptApplicationsDirect
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe AcceptApplicationsMode
|
||||||
|
instance Finite AcceptApplicationsMode
|
||||||
|
|
||||||
|
nullaryPathPiece ''AcceptApplicationsMode $ camelToPathPiece' 2
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''AcceptApplicationsMode id
|
||||||
|
|
||||||
|
data AcceptApplicationsSecondary = AcceptApplicationsSecondaryRandom
|
||||||
|
| AcceptApplicationsSecondaryTime
|
||||||
|
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe AcceptApplicationsSecondary
|
||||||
|
instance Finite AcceptApplicationsSecondary
|
||||||
|
|
||||||
|
nullaryPathPiece ''AcceptApplicationsSecondary $ camelToPathPiece' 3
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''AcceptApplicationsSecondary id
|
||||||
|
|
||||||
|
|
||||||
getCApplicationsR, postCApplicationsR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCApplicationsR, postCApplicationsR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCApplicationsR = postCApplicationsR
|
getCApplicationsR = postCApplicationsR
|
||||||
postCApplicationsR tid ssh csh = do
|
postCApplicationsR tid ssh csh = do
|
||||||
(table, allocationsBounds) <- runDB $ do
|
(table, allocationsBounds, mayAccept) <- runDB $ do
|
||||||
Entity cid Course{..} <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
Entity cid Course{..} <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
|
||||||
csvName <- getMessageRender <*> pure (MsgCourseApplicationsTableCsvName tid ssh csh)
|
csvName <- getMessageRender <*> pure (MsgCourseApplicationsTableCsvName tid ssh csh)
|
||||||
@ -237,31 +284,43 @@ postCApplicationsR tid ssh csh = do
|
|||||||
studyFeatures <- view queryStudyFeatures
|
studyFeatures <- view queryStudyFeatures
|
||||||
studyTerms <- view queryStudyTerms
|
studyTerms <- view queryStudyTerms
|
||||||
studyDegree <- view queryStudyDegree
|
studyDegree <- view queryStudyDegree
|
||||||
|
courseParticipant <- view queryCourseParticipant
|
||||||
|
|
||||||
lift $ do
|
lift $ do
|
||||||
|
E.on $ E.just (user E.^. UserId) E.==. courseParticipant E.?. CourseParticipantUser
|
||||||
|
E.&&. courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val cid)
|
||||||
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
|
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
|
||||||
E.on $ studyTerms E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
|
E.on $ studyTerms E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
|
||||||
E.on $ studyFeatures E.?. StudyFeaturesId E.==. courseApplication E.^. CourseApplicationField
|
E.on $ studyFeatures E.?. StudyFeaturesId E.==. courseApplication E.^. CourseApplicationField
|
||||||
E.on $ courseApplication E.^. CourseApplicationAllocation E.==. allocation E.?. AllocationId
|
E.on $ courseApplication E.^. CourseApplicationAllocation E.==. allocation E.?. AllocationId
|
||||||
E.on $ user E.^. UserId E.==. courseApplication E.^. CourseApplicationUser
|
E.on $ user E.^. UserId E.==. courseApplication E.^. CourseApplicationUser
|
||||||
E.where_ $ courseApplication E.^. CourseApplicationCourse E.==. E.val cid
|
E.&&. courseApplication E.^. CourseApplicationCourse E.==. E.val cid
|
||||||
|
|
||||||
return (courseApplication, user, hasFiles, allocation, studyFeatures, studyTerms, studyDegree)
|
return ( courseApplication
|
||||||
|
, user
|
||||||
|
, hasFiles
|
||||||
|
, allocation
|
||||||
|
, studyFeatures
|
||||||
|
, studyTerms
|
||||||
|
, studyDegree
|
||||||
|
, E.not_ . E.isNothing $ courseParticipant E.?. CourseParticipantId
|
||||||
|
)
|
||||||
|
|
||||||
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) CourseApplicationsTableData
|
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) CourseApplicationsTableData
|
||||||
dbtProj = runReaderT $ do
|
dbtProj = runReaderT $ do
|
||||||
appId <- view $ resultCourseApplication . _entityKey
|
appId <- view $ _dbrOutput . _1 . _entityKey
|
||||||
cID <- encrypt appId
|
cID <- encrypt appId
|
||||||
|
|
||||||
guardM . hasReadAccessTo $ CApplicationR tid ssh csh cID CAEditR
|
guardM . hasReadAccessTo $ CApplicationR tid ssh csh cID CAEditR
|
||||||
|
|
||||||
view id
|
asks $ over (_dbrOutput . _3) E.unValue . over (_dbrOutput . _8) E.unValue
|
||||||
|
|
||||||
dbtRowKey = view $ queryCourseApplication . to (E.^. CourseApplicationId)
|
dbtRowKey = view $ queryCourseApplication . to (E.^. CourseApplicationId)
|
||||||
|
|
||||||
dbtColonnade :: Colonnade Sortable _ _
|
dbtColonnade :: Colonnade Sortable _ _
|
||||||
dbtColonnade = mconcat
|
dbtColonnade = mconcat
|
||||||
[ emptyOpticColonnade (resultAllocation . _entityVal) $ \l -> anchorColonnade (views l allocationLink) $ colAllocationShorthand (l . _allocationShorthand)
|
[ sortable (Just "participant") (i18nCell MsgCourseApplicationIsParticipant) $ bool mempty (cell $ toWidget iconOK) . view resultIsParticipant
|
||||||
|
, emptyOpticColonnade (resultAllocation . _entityVal) $ \l -> anchorColonnade (views l allocationLink) $ colAllocationShorthand (l . _allocationShorthand)
|
||||||
, anchorColonnadeM (views (resultCourseApplication . _entityKey) applicationLink) $ colApplicationId (resultCourseApplication . _entityKey)
|
, anchorColonnadeM (views (resultCourseApplication . _entityKey) applicationLink) $ colApplicationId (resultCourseApplication . _entityKey)
|
||||||
, anchorColonnadeM (views (resultUser . _entityKey) participantLink) $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname)
|
, anchorColonnadeM (views (resultUser . _entityKey) participantLink) $ colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname)
|
||||||
, colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
|
, colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
|
||||||
@ -276,7 +335,8 @@ postCApplicationsR tid ssh csh = do
|
|||||||
]
|
]
|
||||||
|
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
[ sortAllocationShorthand $ queryAllocation . to (E.?. AllocationShorthand)
|
[ singletonMap "participant" . SortColumn $ view queryIsParticipant
|
||||||
|
, sortAllocationShorthand $ queryAllocation . to (E.?. AllocationShorthand)
|
||||||
, sortUserName' $ $(multifocusG 2) (queryUser . to (E.^. UserDisplayName)) (queryUser . to (E.^. UserSurname))
|
, sortUserName' $ $(multifocusG 2) (queryUser . to (E.^. UserDisplayName)) (queryUser . to (E.^. UserSurname))
|
||||||
, sortUserMatriculation $ queryUser . to (E.^. UserMatrikelnummer)
|
, sortUserMatriculation $ queryUser . to (E.^. UserMatrikelnummer)
|
||||||
, sortStudyTerms queryStudyTerms
|
, sortStudyTerms queryStudyTerms
|
||||||
@ -566,12 +626,67 @@ postCApplicationsR tid ssh csh = do
|
|||||||
|| numFirstChoice' /= numFirstChoice
|
|| numFirstChoice' /= numFirstChoice
|
||||||
]
|
]
|
||||||
|
|
||||||
(, allocationsBounds) <$> dbTableWidget' psValidator DBTable{..}
|
mayAccept <- hasWriteAccessTo $ CourseR tid ssh csh CAddUserR
|
||||||
|
|
||||||
|
(, allocationsBounds, mayAccept) <$> dbTableWidget' psValidator DBTable{..}
|
||||||
|
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
let title = prependCourseTitle tid ssh csh MsgCourseApplicationsListTitle
|
let title = prependCourseTitle tid ssh csh MsgCourseApplicationsListTitle
|
||||||
registrationOpen = maybe True (now <)
|
registrationOpen = maybe True (now <)
|
||||||
|
|
||||||
|
|
||||||
|
((acceptRes, acceptWgt'), acceptEnc) <- runFormPost . identifyForm BtnAcceptApplications . renderAForm FormStandard $
|
||||||
|
(,) <$> apopt (selectField optionsFinite) (fslI MsgAcceptApplicationsMode & setTooltip MsgAcceptApplicationsModeTip) (Just AcceptApplicationsInvite)
|
||||||
|
<*> apopt (selectField optionsFinite) (fslI MsgAcceptApplicationsSecondary & setTooltip MsgAcceptApplicationsSecondaryTip) (Just AcceptApplicationsSecondaryTime)
|
||||||
|
|
||||||
|
let acceptWgt = wrapForm' BtnAcceptApplications acceptWgt' def
|
||||||
|
{ formSubmit = FormSubmit
|
||||||
|
, formAction = Just . SomeRoute $ CourseR tid ssh csh CApplicationsR
|
||||||
|
, formEncoding = acceptEnc
|
||||||
|
}
|
||||||
|
|
||||||
|
when mayAccept $
|
||||||
|
formResult acceptRes $ \(invMode, appsSecOrder) -> do
|
||||||
|
runDBJobs $ do
|
||||||
|
Entity cid Course{..} <- getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
participants <- count [ CourseParticipantCourse ==. cid ]
|
||||||
|
let openCapacity = subtract participants <$> courseCapacity
|
||||||
|
|
||||||
|
applications <- E.select . E.from $ \(user `E.InnerJoin` application) -> do
|
||||||
|
E.on $ user E.^. UserId E.==. application E.^. CourseApplicationUser
|
||||||
|
|
||||||
|
E.where_ $ application E.^. CourseApplicationCourse E.==. E.val cid
|
||||||
|
E.&&. E.isNothing (application E.^. CourseApplicationAllocation)
|
||||||
|
E.&&. E.not_ (application E.^. CourseApplicationRatingVeto)
|
||||||
|
E.&&. E.maybe E.true (`E.in_` E.valList (filter (view $ passingGrade . _Wrapped) universeF)) (application E.^. CourseApplicationRatingPoints )
|
||||||
|
|
||||||
|
E.where_ . E.not_ . E.exists . E.from $ \participant ->
|
||||||
|
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
|
||||||
|
E.&&. participant E.^. CourseParticipantUser E.==. user E.^. UserId
|
||||||
|
|
||||||
|
return (user, application)
|
||||||
|
|
||||||
|
let
|
||||||
|
ratingL = _2 . _entityVal . _courseApplicationRatingPoints . to (Down . ExamGradeDefCenter)
|
||||||
|
cmp = case appsSecOrder of
|
||||||
|
AcceptApplicationsSecondaryTime
|
||||||
|
-> comparing . view $ $(multifocusG 2) ratingL (_2 . _entityVal . _courseApplicationTime)
|
||||||
|
AcceptApplicationsSecondaryRandom
|
||||||
|
-> comparing $ view ratingL
|
||||||
|
sortedApplications <- unstableSortBy cmp applications
|
||||||
|
|
||||||
|
let applicants = sortedApplications
|
||||||
|
& nubOn (view $ _1 . _entityKey)
|
||||||
|
& maybe id take openCapacity
|
||||||
|
& setOf (case invMode of
|
||||||
|
AcceptApplicationsDirect -> folded . _1 . _entityKey . to Right
|
||||||
|
AcceptApplicationsInvite -> folded . _1 . _entityVal . _userEmail . to Left
|
||||||
|
)
|
||||||
|
|
||||||
|
mapM_ addMessage' <=< execWriterT $ registerUsers cid applicants
|
||||||
|
redirect $ CourseR tid ssh csh CUsersR
|
||||||
|
|
||||||
|
|
||||||
siteLayoutMsg title $ do
|
siteLayoutMsg title $ do
|
||||||
setTitleI title
|
setTitleI title
|
||||||
$(widgetFile "course/applications-list")
|
$(widgetFile "course/applications-list")
|
||||||
|
|||||||
@ -107,12 +107,13 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
|
|||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
uid <- liftHandler requireAuthId
|
uid <- liftHandler requireAuthId
|
||||||
(lecturerSchools, adminSchools) <- liftHandler . runDB $ do
|
(lecturerSchools, adminSchools, oldSchool) <- liftHandler . runDB $ do
|
||||||
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
|
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
|
||||||
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
|
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
|
||||||
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
|
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
|
||||||
return (lecturerSchools, adminSchools)
|
oldSchool <- forM (cfCourseId =<< template) $ fmap courseSchool . getJust
|
||||||
let userSchools = nub $ lecturerSchools ++ adminSchools
|
return (lecturerSchools, adminSchools, oldSchool)
|
||||||
|
let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
|
||||||
|
|
||||||
termsField <- case template of
|
termsField <- case template of
|
||||||
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
-- Change of term is only allowed if user may delete the course (i.e. no participants) or admin
|
||||||
|
|||||||
@ -4,6 +4,9 @@ module Handler.Course.ParticipantInvite
|
|||||||
( InvitableJunction(..), InvitationDBData(..), InvitationTokenData(..)
|
( InvitableJunction(..), InvitationDBData(..), InvitationTokenData(..)
|
||||||
, getCInviteR, postCInviteR
|
, getCInviteR, postCInviteR
|
||||||
, getCAddUserR, postCAddUserR
|
, getCAddUserR, postCAddUserR
|
||||||
|
, AddParticipantsResult(..)
|
||||||
|
, addParticipantsResultMessages
|
||||||
|
, registerUsers, registerUser
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -96,16 +99,16 @@ participantInvitationConfig = InvitationConfig{..}
|
|||||||
return . SomeMessage $ MsgCourseParticipantInvitationAccepted (CI.original courseName)
|
return . SomeMessage $ MsgCourseParticipantInvitationAccepted (CI.original courseName)
|
||||||
invitationUltDest (Entity _ Course{..}) _ = return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR
|
invitationUltDest (Entity _ Course{..}) _ = return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR
|
||||||
|
|
||||||
data AddRecipientsResult = AddRecipientsResult
|
data AddParticipantsResult = AddParticipantsResult
|
||||||
{ aurAlreadyRegistered
|
{ aurAlreadyRegistered
|
||||||
, aurNoUniquePrimaryField
|
, aurNoUniquePrimaryField
|
||||||
, aurSuccess :: [UserEmail]
|
, aurSuccess :: Set UserId
|
||||||
} deriving (Read, Show, Generic, Typeable)
|
} deriving (Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
instance Semigroup AddRecipientsResult where
|
instance Semigroup AddParticipantsResult where
|
||||||
(<>) = mappenddefault
|
(<>) = mappenddefault
|
||||||
|
|
||||||
instance Monoid AddRecipientsResult where
|
instance Monoid AddParticipantsResult where
|
||||||
mempty = memptydefault
|
mempty = memptydefault
|
||||||
mappend = (<>)
|
mappend = (<>)
|
||||||
|
|
||||||
@ -118,7 +121,9 @@ postCAddUserR tid ssh csh = do
|
|||||||
wreq (multiUserField (maybe True not $ formResultToMaybe enlist) Nothing)
|
wreq (multiUserField (maybe True not $ formResultToMaybe enlist) Nothing)
|
||||||
(fslI MsgCourseParticipantInviteField & setTooltip MsgMultiEmailFieldTip) Nothing
|
(fslI MsgCourseParticipantInviteField & setTooltip MsgMultiEmailFieldTip) Nothing
|
||||||
|
|
||||||
formResultModal usersToEnlist (CourseR tid ssh csh CUsersR) $ processUsers cid
|
formResultModal usersToEnlist (CourseR tid ssh csh CUsersR) $
|
||||||
|
hoist runDBJobs . registerUsers cid
|
||||||
|
|
||||||
|
|
||||||
let heading = prependCourseTitle tid ssh csh MsgCourseParticipantsRegisterHeading
|
let heading = prependCourseTitle tid ssh csh MsgCourseParticipantsRegisterHeading
|
||||||
|
|
||||||
@ -128,57 +133,74 @@ postCAddUserR tid ssh csh = do
|
|||||||
{ formEncoding
|
{ formEncoding
|
||||||
, formAction = Just . SomeRoute $ CourseR tid ssh csh CAddUserR
|
, formAction = Just . SomeRoute $ CourseR tid ssh csh CAddUserR
|
||||||
}
|
}
|
||||||
where
|
|
||||||
processUsers :: CourseId -> Set (Either UserEmail UserId) -> WriterT [Message] Handler ()
|
|
||||||
processUsers cid users = do
|
|
||||||
let (emails,uids) = partitionEithers $ Set.toList users
|
|
||||||
AddRecipientsResult{..} <- lift . runDBJobs $ do
|
|
||||||
-- send Invitation eMails to unkown users
|
|
||||||
sinkInvitationsF participantInvitationConfig [(mail,cid,(InvDBDataParticipant,InvTokenDataParticipant)) | mail <- emails]
|
|
||||||
-- register known users
|
|
||||||
execWriterT $ mapM (registerUser cid) uids
|
|
||||||
|
|
||||||
unless (null emails) $
|
registerUsers :: CourseId -> Set (Either UserEmail UserId) -> WriterT [Message] (YesodJobDB UniWorX) ()
|
||||||
tell . pure <=< messageI Success . MsgCourseParticipantsInvited $ length emails
|
registerUsers cid users = do
|
||||||
|
let (emails,uids) = partitionEithers $ Set.toList users
|
||||||
|
|
||||||
unless (null aurAlreadyRegistered) $ do
|
-- send Invitation eMails to unkown users
|
||||||
let modalTrigger = [whamlet|_{MsgCourseParticipantsAlreadyRegistered (length aurAlreadyRegistered)}|]
|
lift $ sinkInvitationsF participantInvitationConfig [(mail,cid,(InvDBDataParticipant,InvTokenDataParticipant)) | mail <- emails]
|
||||||
modalContent = $(widgetFile "messages/courseInvitationAlreadyRegistered")
|
-- register known users
|
||||||
tell . pure <=< messageWidget Info $ msgModal modalTrigger (Right modalContent)
|
tell <=< lift . addParticipantsResultMessages <=< lift . execWriterT $ mapM_ (registerUser cid) uids
|
||||||
|
|
||||||
unless (null aurNoUniquePrimaryField) $ do
|
unless (null emails) $
|
||||||
let modalTrigger = [whamlet|_{MsgCourseParticipantsRegisteredWithoutField (length aurNoUniquePrimaryField)}|]
|
tell . pure <=< messageI Success . MsgCourseParticipantsInvited $ length emails
|
||||||
modalContent = $(widgetFile "messages/courseInvitationRegisteredWithoutField")
|
|
||||||
tell . pure <=< messageWidget Warning $ msgModal modalTrigger (Right modalContent)
|
|
||||||
|
|
||||||
unless (null aurSuccess) $
|
|
||||||
tell . pure <=< messageI Success . MsgCourseParticipantsRegistered $ length aurSuccess
|
|
||||||
|
|
||||||
registerUser :: CourseId -> UserId -> WriterT AddRecipientsResult (YesodJobDB UniWorX) ()
|
addParticipantsResultMessages :: (MonadHandler m, HandlerSite m ~ UniWorX)
|
||||||
registerUser cid uid = exceptT tell tell $ do
|
=> AddParticipantsResult
|
||||||
User{..} <- lift . lift $ getJust uid
|
-> ReaderT (YesodPersistBackend UniWorX) m [Message]
|
||||||
|
addParticipantsResultMessages AddParticipantsResult{..} = execWriterT $ do
|
||||||
|
(aurAlreadyRegistered', aurNoUniquePrimaryField') <-
|
||||||
|
(,) <$> fmap sort (lift . mapM (fmap userEmail . getJust) $ Set.toList aurAlreadyRegistered)
|
||||||
|
<*> fmap sort (lift . mapM (fmap userEmail . getJust) $ Set.toList aurNoUniquePrimaryField)
|
||||||
|
|
||||||
whenM (lift . lift . existsBy $ UniqueParticipant uid cid) $
|
unless (null aurAlreadyRegistered) $ do
|
||||||
throwError $ mempty { aurAlreadyRegistered = pure userEmail }
|
let modalTrigger = [whamlet|_{MsgCourseParticipantsAlreadyRegistered (length aurAlreadyRegistered)}|]
|
||||||
|
modalContent = $(widgetFile "messages/courseInvitationAlreadyRegistered")
|
||||||
|
tell . pure <=< messageWidget Info $ msgModal modalTrigger (Right modalContent)
|
||||||
|
|
||||||
features <- lift . lift $ selectKeysList [ StudyFeaturesUser ==. uid, StudyFeaturesValid ==. True, StudyFeaturesType ==. FieldPrimary ] []
|
unless (null aurNoUniquePrimaryField) $ do
|
||||||
|
let modalTrigger = [whamlet|_{MsgCourseParticipantsRegisteredWithoutField (length aurNoUniquePrimaryField)}|]
|
||||||
|
modalContent = $(widgetFile "messages/courseInvitationRegisteredWithoutField")
|
||||||
|
tell . pure <=< messageWidget Warning $ msgModal modalTrigger (Right modalContent)
|
||||||
|
|
||||||
let courseParticipantField
|
unless (null aurSuccess) $
|
||||||
| [f] <- features = Just f
|
tell . pure <=< messageI Success . MsgCourseParticipantsRegistered $ length aurSuccess
|
||||||
| otherwise = Nothing
|
|
||||||
|
|
||||||
courseParticipantRegistration <- liftIO getCurrentTime
|
|
||||||
void . lift . lift . insert $ CourseParticipant
|
|
||||||
{ courseParticipantCourse = cid
|
|
||||||
, courseParticipantUser = uid
|
|
||||||
, courseParticipantAllocated = False
|
|
||||||
, ..
|
|
||||||
}
|
|
||||||
lift . lift . audit $ TransactionCourseParticipantEdit cid uid
|
|
||||||
|
|
||||||
return $ case courseParticipantField of
|
registerUser :: (MonadHandler m, HandlerSite m ~ UniWorX, MonadCatch m)
|
||||||
Nothing -> mempty { aurNoUniquePrimaryField = pure userEmail }
|
=> CourseId
|
||||||
Just _ -> mempty { aurSuccess = pure userEmail }
|
-> UserId
|
||||||
|
-> WriterT AddParticipantsResult (ReaderT (YesodPersistBackend UniWorX) m) ()
|
||||||
|
registerUser cid uid = exceptT tell tell $ do
|
||||||
|
whenM (lift . lift . existsBy $ UniqueParticipant uid cid) $
|
||||||
|
throwError $ mempty { aurAlreadyRegistered = Set.singleton uid }
|
||||||
|
|
||||||
|
features <- lift . lift $ selectKeysList [ StudyFeaturesUser ==. uid, StudyFeaturesValid ==. True, StudyFeaturesType ==. FieldPrimary ] []
|
||||||
|
applications <- lift . lift $ selectList [ CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] []
|
||||||
|
|
||||||
|
let courseParticipantField
|
||||||
|
| [f] <- features
|
||||||
|
= Just f
|
||||||
|
| [f'] <- nub $ mapMaybe (courseApplicationField . entityVal) applications
|
||||||
|
, f' `elem` features
|
||||||
|
= Just f'
|
||||||
|
| otherwise
|
||||||
|
= Nothing
|
||||||
|
|
||||||
|
courseParticipantRegistration <- liftIO getCurrentTime
|
||||||
|
void . lift . lift . insert $ CourseParticipant
|
||||||
|
{ courseParticipantCourse = cid
|
||||||
|
, courseParticipantUser = uid
|
||||||
|
, courseParticipantAllocated = False
|
||||||
|
, ..
|
||||||
|
}
|
||||||
|
lift . lift . audit $ TransactionCourseParticipantEdit cid uid
|
||||||
|
|
||||||
|
return $ case courseParticipantField of
|
||||||
|
Nothing -> mempty { aurNoUniquePrimaryField = Set.singleton uid }
|
||||||
|
Just _ -> mempty { aurSuccess = Set.singleton uid }
|
||||||
|
|
||||||
|
|
||||||
getCInviteR, postCInviteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCInviteR, postCInviteR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
|||||||
@ -72,12 +72,18 @@ getEShowR tid ssh csh examn = do
|
|||||||
examClosedShown = lecturerInfoShown
|
examClosedShown = lecturerInfoShown
|
||||||
|
|
||||||
sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, let Just mPoints = examPartMaxPoints ]
|
sumMaxPoints = sum [ fromRational examPartWeight * mPoints | Entity _ ExamPart{..} <- examParts, let Just mPoints = examPartMaxPoints ]
|
||||||
sumPoints = getSum <$> foldMap (fmap Sum . examPartResultResult . entityVal) results
|
|
||||||
|
|
||||||
noBonus = fromMaybe False $ do
|
noBonus = fromMaybe False $ do
|
||||||
guardM $ bonusOnlyPassed <$> examBonusRule
|
guardM $ bonusOnlyPassed <$> examBonusRule
|
||||||
return . fromMaybe True $ result ^? _Just . _entityVal . _examResultResult . _examResult . passingGrade . _Wrapped . to not
|
return . fromMaybe True $ result ^? _Just . _entityVal . _examResultResult . _examResult . passingGrade . _Wrapped . to not
|
||||||
|
|
||||||
|
sumPoints = fmap getSum . mconcat $ catMaybes
|
||||||
|
[ Just $ foldMap (fmap Sum . examPartResultResult . entityVal) results
|
||||||
|
, guard (not noBonus) *> fmap (pure . Sum . examBonusBonus . entityVal) bonus
|
||||||
|
]
|
||||||
|
|
||||||
|
hasRegistration = any snd occurrences
|
||||||
|
|
||||||
|
|
||||||
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
|
let examTimes = all (\(Entity _ ExamOccurrence{..}, _) -> Just examOccurrenceStart == examStart && examOccurrenceEnd == examEnd) occurrences
|
||||||
registerWidget
|
registerWidget
|
||||||
|
|||||||
@ -160,7 +160,7 @@ resultCourseNote = _dbrOutput . _10 . _Just
|
|||||||
|
|
||||||
|
|
||||||
resultAutomaticExamBonus :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData Points
|
resultAutomaticExamBonus :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData Points
|
||||||
resultAutomaticExamBonus exam examBonus' = resultUser . _entityKey . folding (\uid -> examResultBonus <$> examBonusRule exam <*> examBonusPossible uid examBonus' <*> examBonusAchieved uid examBonus')
|
resultAutomaticExamBonus exam examBonus' = resultUser . _entityKey . folding (\uid -> examResultBonus <$> examBonusRule exam <*> pure (examBonusPossible uid examBonus') <*> pure (examBonusAchieved uid examBonus'))
|
||||||
|
|
||||||
resultAutomaticExamResult :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData ExamResultGrade
|
resultAutomaticExamResult :: Exam -> Map UserId SheetTypeSummary -> Fold ExamUserTableData ExamResultGrade
|
||||||
resultAutomaticExamResult exam examBonus' = folding . runReader $ do
|
resultAutomaticExamResult exam examBonus' = folding . runReader $ do
|
||||||
@ -396,7 +396,7 @@ postEUsersR tid ssh csh examn = do
|
|||||||
allBoni :: SheetGradeSummary
|
allBoni :: SheetGradeSummary
|
||||||
allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus
|
allBoni = (mappend <$> normalSummary <*> bonusSummary) $ fold bonus
|
||||||
|
|
||||||
doBonus = is _Just examGradingRule || is _Just examBonusRule
|
doBonus = is _Just examBonusRule
|
||||||
showPasses = doBonus && numSheetsPasses allBoni /= 0
|
showPasses = doBonus && numSheetsPasses allBoni /= 0
|
||||||
showPoints = doBonus && getSum (numSheetsPoints allBoni) /= 0
|
showPoints = doBonus && getSum (numSheetsPoints allBoni) /= 0
|
||||||
|
|
||||||
@ -494,14 +494,14 @@ postEUsersR tid ssh csh examn = do
|
|||||||
, pure $ colDegreeShort resultStudyDegree
|
, pure $ colDegreeShort resultStudyDegree
|
||||||
, pure $ colFeaturesSemester resultStudyFeatures
|
, pure $ colFeaturesSemester resultStudyFeatures
|
||||||
, pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
|
, pure $ sortable (Just "occurrence") (i18nCell MsgExamOccurrence) $ maybe mempty (anchorCell' (\n -> CExamR tid ssh csh examn EShowR :#: [st|exam-occurrence__#{n}|]) id . examOccurrenceName . entityVal) . view _userTableOccurrence
|
||||||
, guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
|
, guardOn showPasses $ sortable Nothing (i18nCell MsgAchievedPasses) $ \(view $ resultUser . _entityKey -> uid) ->
|
||||||
SheetGradeSummary{achievedPasses} <- examBonusAchieved uid bonus
|
let SheetGradeSummary{achievedPasses} = examBonusAchieved uid bonus
|
||||||
SheetGradeSummary{numSheetsPasses} <- examBonusPossible uid bonus
|
SheetGradeSummary{numSheetsPasses} = examBonusPossible uid bonus
|
||||||
return $ propCell (getSum achievedPasses) (getSum numSheetsPasses)
|
in propCell (getSum achievedPasses) (getSum numSheetsPasses)
|
||||||
, guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) -> fromMaybe mempty $ do
|
, guardOn showPoints $ sortable Nothing (i18nCell MsgAchievedPoints) $ \(view $ resultUser . _entityKey -> uid) ->
|
||||||
SheetGradeSummary{achievedPoints} <- examBonusAchieved uid bonus
|
let SheetGradeSummary{achievedPoints} = examBonusAchieved uid bonus
|
||||||
SheetGradeSummary{sumSheetsPoints} <- examBonusPossible uid bonus
|
SheetGradeSummary{sumSheetsPoints} = examBonusPossible uid bonus
|
||||||
return $ propCell (getSum achievedPoints) (getSum sumSheetsPoints)
|
in propCell (getSum achievedPoints) (getSum sumSheetsPoints)
|
||||||
, guardOn doBonus $ sortable (Just "bonus") (i18nCell MsgExamBonusAchieved) . automaticCell $ resultExamBonus . _entityVal . _examBonusBonus . to Right <> resultAutomaticExamBonus' . to Left
|
, guardOn doBonus $ sortable (Just "bonus") (i18nCell MsgExamBonusAchieved) . automaticCell $ resultExamBonus . _entityVal . _examBonusBonus . to Right <> resultAutomaticExamBonus' . to Left
|
||||||
, pure $ mconcat
|
, pure $ mconcat
|
||||||
[ sortable (Just $ fromText [st|part-#{toPathPiece examPartNumber}|]) (i18nCell $ MsgExamPartNumbered examPartNumber) $ maybe mempty i18nCell . preview (resultExamPartResult epId . _Just . _entityVal . _examPartResultResult)
|
[ sortable (Just $ fromText [st|part-#{toPathPiece examPartNumber}|]) (i18nCell $ MsgExamPartNumbered examPartNumber) $ maybe mempty i18nCell . preview (resultExamPartResult epId . _Just . _entityVal . _examPartResultResult)
|
||||||
@ -612,10 +612,10 @@ postEUsersR tid ssh csh examn = do
|
|||||||
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
|
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
|
||||||
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
|
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
|
||||||
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceName)
|
||||||
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPoints . _Wrapped) (bool (const Nothing) Just showPoints)
|
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPoints . _Wrapped) (bool (const Nothing) Just showPoints)
|
||||||
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _Just . _achievedPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
|
<*> previews (resultUser . _entityKey . to (examBonusAchieved ?? bonus) . _achievedPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
|
||||||
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _sumSheetsPoints . _Wrapped) (bool (const Nothing) Just showPoints)
|
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _sumSheetsPoints . _Wrapped) (bool (const Nothing) Just showPoints)
|
||||||
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _Just . _numSheetsPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
|
<*> previews (resultUser . _entityKey . to (examBonusPossible ?? bonus) . _numSheetsPasses . _Wrapped . integral) (bool (const Nothing) Just showPasses)
|
||||||
<*> previews (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus') (bool (const Nothing) Just doBonus)
|
<*> previews (resultExamBonus . _entityVal . _examBonusBonus <> resultAutomaticExamBonus') (bool (const Nothing) Just doBonus)
|
||||||
<*> (Map.fromList . map (over _1 examPartNumber . over (_2 . _Just) (examPartResultResult . entityVal)) <$> asks (toListOf resultExamParts))
|
<*> (Map.fromList . map (over _1 examPartNumber . over (_2 . _Just) (examPartResultResult . entityVal)) <$> asks (toListOf resultExamParts))
|
||||||
<*> previews (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') resultView
|
<*> previews (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') resultView
|
||||||
@ -645,7 +645,7 @@ postEUsersR tid ssh csh examn = do
|
|||||||
when (epNumber `elem` examPartNumbers) $
|
when (epNumber `elem` examPartNumbers) $
|
||||||
yield $ ExamUserCsvSetPartResultData uid epNumber (Just epRes)
|
yield $ ExamUserCsvSetPartResultData uid epNumber (Just epRes)
|
||||||
|
|
||||||
when (is _Just . join $ csvEUserBonus dbCsvNew) $
|
when (doBonus && is _Just (join $ csvEUserBonus dbCsvNew)) $
|
||||||
yield . ExamUserCsvSetBonusData False uid . join $ csvEUserBonus dbCsvNew
|
yield . ExamUserCsvSetBonusData False uid . join $ csvEUserBonus dbCsvNew
|
||||||
|
|
||||||
when (is _Just $ csvEUserExamResult dbCsvNew) $
|
when (is _Just $ csvEUserExamResult dbCsvNew) $
|
||||||
@ -684,15 +684,18 @@ postEUsersR tid ssh csh examn = do
|
|||||||
newResult = fmap resultView <$> examGrade examVal (newBonus <|> oldBonus) =<< newResults
|
newResult = fmap resultView <$> examGrade examVal (newBonus <|> oldBonus) =<< newResults
|
||||||
oldResult = dbCsvOld ^? (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') . to resultView
|
oldResult = dbCsvOld ^? (resultExamResult . _entityVal . _examResultResult <> resultAutomaticExamResult') . to resultView
|
||||||
|
|
||||||
case newBonus of
|
when doBonus $
|
||||||
_ | newBonus == oldBonus
|
case newBonus of
|
||||||
-> return ()
|
_ | newBonus == oldBonus
|
||||||
_ | is _Nothing newBonus
|
-> return ()
|
||||||
-> return ()
|
_ | is _Nothing newBonus
|
||||||
Nothing
|
-> return ()
|
||||||
-> yield $ ExamUserCsvSetBonusData False uid newBonus
|
_ | Just ExamBonusManual{} <- examBonusRule
|
||||||
Just _
|
-> yield $ ExamUserCsvSetBonusData False uid newBonus
|
||||||
-> yield $ ExamUserCsvSetBonusData True uid newBonus
|
Nothing
|
||||||
|
-> yield $ ExamUserCsvSetBonusData False uid newBonus
|
||||||
|
Just _
|
||||||
|
-> yield $ ExamUserCsvSetBonusData True uid newBonus
|
||||||
|
|
||||||
case newResult of
|
case newResult of
|
||||||
_ | csvEUserExamResult dbCsvNew == oldResult
|
_ | csvEUserExamResult dbCsvNew == oldResult
|
||||||
@ -928,22 +931,31 @@ postEUsersR tid ssh csh examn = do
|
|||||||
guessUser :: ExamUserTableCsv -> DB (Bool, UserId)
|
guessUser :: ExamUserTableCsv -> DB (Bool, UserId)
|
||||||
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do
|
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do
|
||||||
users <- E.select . E.from $ \user -> do
|
users <- E.select . E.from $ \user -> do
|
||||||
E.where_ . E.and $ catMaybes
|
E.where_ . E.or $ catMaybes
|
||||||
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation
|
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation
|
||||||
, (user E.^. UserDisplayName E.==.) . E.val <$> csvEUserName
|
, (user E.^. UserDisplayName `E.hasInfix`) . E.val <$> csvEUserName
|
||||||
, (user E.^. UserSurname E.==.) . E.val <$> csvEUserSurname
|
, (user E.^. UserSurname `E.hasInfix`) . E.val <$> csvEUserSurname
|
||||||
, (user E.^. UserFirstName E.==.) . E.val <$> csvEUserFirstName
|
, (user E.^. UserFirstName `E.hasInfix`) . E.val <$> csvEUserFirstName
|
||||||
]
|
]
|
||||||
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
|
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
|
||||||
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
|
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
|
||||||
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
|
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
|
||||||
E.limit 2
|
return (isCourseParticipant, user)
|
||||||
return (isCourseParticipant, user E.^. UserId)
|
let users' = reverse $ sortBy closeness users
|
||||||
case users of
|
closeness :: (E.Value Bool, Entity User) -> (E.Value Bool, Entity User) -> Ordering
|
||||||
(filter . view $ _1 . _Value -> [(E.Value isPart, E.Value uid)])
|
closeness = mconcat $ catMaybes
|
||||||
-> return (isPart, uid)
|
[ pure $ comparing (preview $ _2 . _entityVal . _userMatrikelnummer . only csvEUserMatriculation)
|
||||||
[(E.Value isPart, E.Value uid)]
|
, pure $ comparing (view _1)
|
||||||
-> return (isPart, uid)
|
, csvEUserSurname <&> \surn -> comparing (preview $ _2 . _entityVal . _userSurname . to CI.mk . only (CI.mk surn))
|
||||||
|
, csvEUserFirstName <&> \firstn -> comparing (preview $ _2 . _entityVal . _userFirstName . to CI.mk . only (CI.mk firstn))
|
||||||
|
, csvEUserName <&> \dispn -> comparing (preview $ _2 . _entityVal . _userDisplayName . to CI.mk . only (CI.mk dispn))
|
||||||
|
]
|
||||||
|
case users' of
|
||||||
|
[(E.Value isPart, Entity uid _)]
|
||||||
|
-> return (isPart, uid)
|
||||||
|
(x@(E.Value isPart, Entity uid _) : x' : _)
|
||||||
|
| GT <- x `closeness` x'
|
||||||
|
-> return (isPart, uid)
|
||||||
_other
|
_other
|
||||||
-> throwM ExamUserCsvExceptionNoMatchingUser
|
-> throwM ExamUserCsvExceptionNoMatchingUser
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,11 @@
|
|||||||
module Handler.Profile where
|
module Handler.Profile
|
||||||
|
( getProfileR, postProfileR
|
||||||
|
, getProfileDataR, makeProfileData
|
||||||
|
, getAuthPredsR, postAuthPredsR
|
||||||
|
, getUserNotificationR, postUserNotificationR
|
||||||
|
, getSetDisplayEmailR, postSetDisplayEmailR
|
||||||
|
, getCsvOptionsR, postCsvOptionsR
|
||||||
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
|
||||||
@ -796,3 +803,25 @@ postSetDisplayEmailR = do
|
|||||||
siteLayoutMsg MsgTitleChangeUserDisplayEmail $ do
|
siteLayoutMsg MsgTitleChangeUserDisplayEmail $ do
|
||||||
setTitleI MsgTitleChangeUserDisplayEmail
|
setTitleI MsgTitleChangeUserDisplayEmail
|
||||||
$(i18nWidgetFile "set-display-email")
|
$(i18nWidgetFile "set-display-email")
|
||||||
|
|
||||||
|
getCsvOptionsR, postCsvOptionsR :: Handler Html
|
||||||
|
getCsvOptionsR = postCsvOptionsR
|
||||||
|
postCsvOptionsR = do
|
||||||
|
Entity uid User{userCsvOptions} <- requireAuth
|
||||||
|
|
||||||
|
((optionsRes, optionsWgt'), optionsEnctype) <- runFormPost . renderAForm FormStandard $
|
||||||
|
csvOptionsForm (fslI MsgCsvOptions & setTooltip MsgCsvOptionsTip) (Just userCsvOptions)
|
||||||
|
|
||||||
|
formResultModal optionsRes CsvOptionsR $ \opts -> do
|
||||||
|
lift . runDB $ update uid [ UserCsvOptions =. opts ]
|
||||||
|
tell . pure =<< messageI Success MsgCsvOptionsUpdated
|
||||||
|
|
||||||
|
siteLayoutMsg MsgCsvOptions $ do
|
||||||
|
setTitleI MsgCsvOptions
|
||||||
|
|
||||||
|
isModal <- hasCustomHeader HeaderIsModal
|
||||||
|
wrapForm optionsWgt' def
|
||||||
|
{ formAction = Just $ SomeRoute CsvOptionsR
|
||||||
|
, formEncoding = optionsEnctype
|
||||||
|
, formAttrs = [ asyncSubmitAttr | isModal ]
|
||||||
|
}
|
||||||
|
|||||||
@ -686,7 +686,7 @@ defaultLoads shid = do
|
|||||||
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
|
return (sheetCorrector E.^. SheetCorrectorUser, sheetCorrector E.^. SheetCorrectorLoad, sheetCorrector E.^. SheetCorrectorState)
|
||||||
where
|
where
|
||||||
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
|
toMap :: [(E.Value UserId, E.Value Load, E.Value CorrectorState)] -> Loads
|
||||||
toMap = foldMap $ \(E.Value uid, E.Value load, E.Value state) -> Map.singleton (Right uid) (state, load)
|
toMap = foldMap $ \(E.Value uid, E.Value cLoad, E.Value cState) -> Map.singleton (Right uid) (cState, cLoad)
|
||||||
|
|
||||||
|
|
||||||
correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector))
|
correctorForm :: SheetId -> AForm Handler (Set (Either (Invitation' SheetCorrector) SheetCorrector))
|
||||||
@ -809,7 +809,7 @@ correctorForm shid = wFormToAForm $ do
|
|||||||
|
|
||||||
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector
|
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either (Invitation' SheetCorrector) SheetCorrector
|
||||||
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
|
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
|
||||||
postProcess' (Left email, (state, load)) = Left (email, shid, (InvDBDataSheetCorrector load state, InvTokenDataSheetCorrector))
|
postProcess' (Left email, (cState, load)) = Left (email, shid, (InvDBDataSheetCorrector load cState, InvTokenDataSheetCorrector))
|
||||||
|
|
||||||
filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
|
filledData :: Maybe (Map ListPosition (Either UserEmail UserId, (CorrectorState, Load)))
|
||||||
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
|
filledData = Just . Map.fromList . zip [0..] $ Map.toList loads -- TODO orderBy Name?!
|
||||||
@ -906,7 +906,7 @@ correctorInvitationConfig = InvitationConfig{..}
|
|||||||
itAuthority <- liftHandler requireAuthId
|
itAuthority <- liftHandler requireAuthId
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ (InvDBDataSheetCorrector load state, _) _ = pure $ (JunctionSheetCorrector load state, ())
|
invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted sheetName
|
invitationSuccessMsg (Entity _ Sheet{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted sheetName
|
||||||
invitationUltDest (Entity _ Sheet{..}) _ = do
|
invitationUltDest (Entity _ Sheet{..}) _ = do
|
||||||
|
|||||||
@ -1,480 +1,13 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
|
|
||||||
module Handler.Tutorial
|
module Handler.Tutorial
|
||||||
( module Handler.Tutorial
|
( module Handler.Tutorial
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Handler.Tutorial.Communication as Handler.Tutorial
|
||||||
import Handler.Utils
|
import Handler.Tutorial.Delete as Handler.Tutorial
|
||||||
import Handler.Utils.Tutorial
|
import Handler.Tutorial.Edit as Handler.Tutorial
|
||||||
import Handler.Utils.Delete
|
import Handler.Tutorial.Form as Handler.Tutorial
|
||||||
import Handler.Utils.Communication
|
import Handler.Tutorial.List as Handler.Tutorial
|
||||||
import Handler.Utils.Form.Occurrences
|
import Handler.Tutorial.New as Handler.Tutorial
|
||||||
import Handler.Utils.Invitations
|
import Handler.Tutorial.Register as Handler.Tutorial
|
||||||
import Jobs.Queue
|
import Handler.Tutorial.TutorInvite as Handler.Tutorial
|
||||||
|
import Handler.Tutorial.Users as Handler.Tutorial
|
||||||
import qualified Database.Esqueleto as E
|
|
||||||
import qualified Database.Esqueleto.Utils as E
|
|
||||||
import Database.Esqueleto.Utils.TH
|
|
||||||
|
|
||||||
import Data.Map ((!))
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
|
|
||||||
import Data.Aeson hiding (Result(..))
|
|
||||||
import Text.Hamlet (ihamlet)
|
|
||||||
|
|
||||||
import Handler.Tutorial.Users as Handler.Tutorial
|
|
||||||
|
|
||||||
{-# ANN module ("Hlint: ignore Redundant void" :: String) #-}
|
|
||||||
|
|
||||||
|
|
||||||
getCTutorialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
|
||||||
getCTutorialListR tid ssh csh = do
|
|
||||||
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
|
|
||||||
|
|
||||||
let
|
|
||||||
tutorialDBTable = DBTable{..}
|
|
||||||
where
|
|
||||||
dbtSQLQuery tutorial = do
|
|
||||||
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
|
||||||
let participants = E.sub_select . E.from $ \tutorialParticipant -> do
|
|
||||||
E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
|
||||||
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
|
|
||||||
return (tutorial, participants)
|
|
||||||
dbtRowKey = (E.^. TutorialId)
|
|
||||||
dbtProj = return . over (_dbrOutput . _2) E.unValue
|
|
||||||
dbtColonnade = dbColonnade $ mconcat
|
|
||||||
[ sortable (Just "type") (i18nCell MsgTutorialType) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell $ CI.original tutorialType
|
|
||||||
, sortable (Just "name") (i18nCell MsgTutorialName) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|]
|
|
||||||
, sortable Nothing (i18nCell MsgTutorialTutors) $ \DBRow{ dbrOutput = (Entity tutid _, _) } -> sqlCell $ do
|
|
||||||
tutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do
|
|
||||||
E.on $ tutor E.^. TutorUser E.==. user E.^. UserId
|
|
||||||
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
|
|
||||||
return (user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname)
|
|
||||||
return [whamlet|
|
|
||||||
$newline never
|
|
||||||
<ul .list--iconless .list--inline .list--comma-separated>
|
|
||||||
$forall tutor <- tutors
|
|
||||||
<li>
|
|
||||||
^{nameEmailWidget' tutor}
|
|
||||||
|]
|
|
||||||
, sortable (Just "participants") (i18nCell MsgTutorialParticipants) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, n) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) $ tshow n
|
|
||||||
, sortable (Just "capacity") (i18nCell MsgTutorialCapacity) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . tshow) tutorialCapacity
|
|
||||||
, sortable (Just "room") (i18nCell MsgTutorialRoom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell tutorialRoom
|
|
||||||
, sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> occurrencesCell tutorialTime
|
|
||||||
, sortable (Just "register-group") (i18nCell MsgTutorialRegGroup) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . CI.original) tutorialRegGroup
|
|
||||||
, 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 "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialDeregisterUntil
|
|
||||||
, sortable Nothing mempty $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> cell $ do
|
|
||||||
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
|
|
||||||
]
|
|
||||||
dbtSorting = Map.fromList
|
|
||||||
[ ("type", SortColumn $ \tutorial -> tutorial E.^. TutorialType )
|
|
||||||
, ("name", SortColumn $ \tutorial -> tutorial E.^. TutorialName )
|
|
||||||
, ("participants", SortColumn $ \tutorial -> E.sub_select . E.from $ \tutorialParticipant -> do
|
|
||||||
E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
|
||||||
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
|
|
||||||
)
|
|
||||||
, ("capacity", SortColumn $ \tutorial -> tutorial E.^. TutorialCapacity )
|
|
||||||
, ("room", SortColumn $ \tutorial -> tutorial E.^. TutorialRoom )
|
|
||||||
, ("register-group", SortColumn $ \tutorial -> tutorial E.^. TutorialRegGroup )
|
|
||||||
, ("register-from", SortColumn $ \tutorial -> tutorial E.^. TutorialRegisterFrom )
|
|
||||||
, ("register-to", SortColumn $ \tutorial -> tutorial E.^. TutorialRegisterTo )
|
|
||||||
, ("deregister-until", SortColumn $ \tutorial -> tutorial E.^. TutorialDeregisterUntil )
|
|
||||||
]
|
|
||||||
dbtFilter = Map.empty
|
|
||||||
dbtFilterUI = const mempty
|
|
||||||
dbtStyle = def
|
|
||||||
dbtParams = def
|
|
||||||
dbtIdent :: Text
|
|
||||||
dbtIdent = "tutorials"
|
|
||||||
dbtCsvEncode = noCsvEncode
|
|
||||||
dbtCsvDecode = Nothing
|
|
||||||
|
|
||||||
tutorialDBTableValidator = def
|
|
||||||
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
|
||||||
((), tutorialTable) <- runDB $ dbTable tutorialDBTableValidator tutorialDBTable
|
|
||||||
|
|
||||||
siteLayoutMsg (prependCourseTitle tid ssh csh MsgTutorialsHeading) $ do
|
|
||||||
setTitleI $ prependCourseTitle tid ssh csh MsgTutorialsHeading
|
|
||||||
$(widgetFile "tutorial-list")
|
|
||||||
|
|
||||||
postTRegisterR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler ()
|
|
||||||
postTRegisterR tid ssh csh tutn = do
|
|
||||||
uid <- requireAuthId
|
|
||||||
|
|
||||||
Entity tutid Tutorial{..} <- runDB $ fetchTutorial tid ssh csh tutn
|
|
||||||
|
|
||||||
((btnResult, _), _) <- runFormPost buttonForm
|
|
||||||
|
|
||||||
formResult btnResult $ \case
|
|
||||||
BtnRegister -> do
|
|
||||||
runDB . void . insert $ TutorialParticipant tutid uid
|
|
||||||
addMessageI Success $ MsgTutorialRegisteredSuccess tutorialName
|
|
||||||
redirect $ CourseR tid ssh csh CShowR
|
|
||||||
BtnDeregister -> do
|
|
||||||
runDB . deleteBy $ UniqueTutorialParticipant tutid uid
|
|
||||||
addMessageI Success $ MsgTutorialDeregisteredSuccess tutorialName
|
|
||||||
redirect $ CourseR tid ssh csh CShowR
|
|
||||||
|
|
||||||
invalidArgs ["Register/Deregister button required"]
|
|
||||||
|
|
||||||
getTDeleteR, postTDeleteR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
|
||||||
getTDeleteR = postTDeleteR
|
|
||||||
postTDeleteR tid ssh csh tutn = do
|
|
||||||
tutid <- runDB $ fetchTutorialId tid ssh csh tutn
|
|
||||||
deleteR DeleteRoute
|
|
||||||
{ drRecords = Set.singleton tutid
|
|
||||||
, drUnjoin = \(_ `E.InnerJoin` tutorial) -> tutorial
|
|
||||||
, drGetInfo = \(course `E.InnerJoin` tutorial) -> do
|
|
||||||
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
|
|
||||||
let participants = E.sub_select . E.from $ \participant -> do
|
|
||||||
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
|
||||||
return E.countRows
|
|
||||||
return (course, tutorial, participants :: E.SqlExpr (E.Value Int))
|
|
||||||
, drRenderRecord = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
|
|
||||||
return [whamlet|_{prependCourseTitle courseTerm courseSchool courseShorthand (CI.original tutorialName)} (_{MsgParticipantsN ps})|]
|
|
||||||
, drRecordConfirmString = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
|
|
||||||
return [st|#{termToText (unTermKey courseTerm)}/#{unSchoolKey courseSchool}/#{courseShorthand}/#{tutorialName}+#{tshow ps}|]
|
|
||||||
, drCaption = SomeMessage MsgTutorialDeleteQuestion
|
|
||||||
, drSuccessMessage = SomeMessage MsgTutorialDeleted
|
|
||||||
, drAbort = SomeRoute $ CTutorialR tid ssh csh tutn TUsersR
|
|
||||||
, drSuccess = SomeRoute $ CourseR tid ssh csh CTutorialListR
|
|
||||||
, drDelete = \_ -> id -- TODO: audit
|
|
||||||
}
|
|
||||||
|
|
||||||
getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
|
||||||
getTCommR = postTCommR
|
|
||||||
postTCommR tid ssh csh tutn = do
|
|
||||||
jSender <- requireAuthId
|
|
||||||
(cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn
|
|
||||||
|
|
||||||
commR CommunicationRoute
|
|
||||||
{ crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading
|
|
||||||
, crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR
|
|
||||||
, crJobs = \Communication{..} -> do
|
|
||||||
let jSubject = cSubject
|
|
||||||
jMailContent = cBody
|
|
||||||
jCourse = cid
|
|
||||||
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
|
|
||||||
jMailObjectUUID <- liftIO getRandom
|
|
||||||
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
|
|
||||||
Left email -> return . Address Nothing $ CI.original email
|
|
||||||
Right rid -> userAddress <$> getJust rid
|
|
||||||
forM_ allRecipients $ \jRecipientEmail ->
|
|
||||||
yield JobSendCourseCommunication{..}
|
|
||||||
, crRecipients = Map.fromList
|
|
||||||
[ ( RGTutorialParticipants
|
|
||||||
, E.from $ \(user `E.InnerJoin` participant) -> do
|
|
||||||
E.on $ user E.^. UserId E.==. participant E.^. TutorialParticipantUser
|
|
||||||
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
|
|
||||||
return user
|
|
||||||
)
|
|
||||||
, ( RGCourseLecturers
|
|
||||||
, E.from $ \(user `E.InnerJoin` lecturer) -> do
|
|
||||||
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
|
||||||
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
|
||||||
return user
|
|
||||||
)
|
|
||||||
, ( RGCourseCorrectors
|
|
||||||
, E.from $ \user -> do
|
|
||||||
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
|
|
||||||
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
|
||||||
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
|
|
||||||
E.&&. corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
|
||||||
return user
|
|
||||||
)
|
|
||||||
, ( RGCourseTutors
|
|
||||||
, E.from $ \user -> do
|
|
||||||
E.where_ $ E.exists $ E.from $ \(tutorial `E.InnerJoin` tutor) -> do
|
|
||||||
E.on $ tutorial E.^. TutorialId E.==. tutor E.^. TutorTutorial
|
|
||||||
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
|
||||||
E.&&. tutor E.^. TutorUser E.==. user E.^. UserId
|
|
||||||
return user
|
|
||||||
)
|
|
||||||
]
|
|
||||||
, crRecipientAuth = Just $ \uid -> do
|
|
||||||
isTutorialUser <- E.selectExists . E.from $ \tutorialUser ->
|
|
||||||
E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid
|
|
||||||
E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid
|
|
||||||
|
|
||||||
isAssociatedCorrector <- evalAccessForDB (Just uid) (CourseR tid ssh csh CNotesR) False
|
|
||||||
isAssociatedTutor <- evalAccessForDB (Just uid) (CourseR tid ssh csh CTutorialListR) False
|
|
||||||
|
|
||||||
mr <- getMsgRenderer
|
|
||||||
return $ if
|
|
||||||
| isTutorialUser -> Authorized
|
|
||||||
| otherwise -> orAR mr isAssociatedCorrector isAssociatedTutor
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
instance IsInvitableJunction Tutor where
|
|
||||||
type InvitationFor Tutor = Tutorial
|
|
||||||
data InvitableJunction Tutor = JunctionTutor
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
|
||||||
data InvitationDBData Tutor = InvDBDataTutor
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
|
||||||
data InvitationTokenData Tutor = InvTokenDataTutor
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
|
||||||
|
|
||||||
_InvitableJunction = iso
|
|
||||||
(\Tutor{..} -> (tutorUser, tutorTutorial, JunctionTutor))
|
|
||||||
(\(tutorUser, tutorTutorial, JunctionTutor) -> Tutor{..})
|
|
||||||
|
|
||||||
instance ToJSON (InvitableJunction Tutor) where
|
|
||||||
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
|
||||||
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
|
||||||
instance FromJSON (InvitableJunction Tutor) where
|
|
||||||
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
|
||||||
|
|
||||||
instance ToJSON (InvitationDBData Tutor) where
|
|
||||||
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
|
||||||
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
|
||||||
instance FromJSON (InvitationDBData Tutor) where
|
|
||||||
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
|
||||||
|
|
||||||
instance ToJSON (InvitationTokenData Tutor) where
|
|
||||||
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
|
||||||
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
|
||||||
instance FromJSON (InvitationTokenData Tutor) where
|
|
||||||
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
|
||||||
|
|
||||||
tutorInvitationConfig :: InvitationConfig Tutor
|
|
||||||
tutorInvitationConfig = InvitationConfig{..}
|
|
||||||
where
|
|
||||||
invitationRoute (Entity _ Tutorial{..}) _ = do
|
|
||||||
Course{..} <- get404 tutorialCourse
|
|
||||||
return $ CTutorialR courseTerm courseSchool courseShorthand tutorialName TInviteR
|
|
||||||
invitationResolveFor _ = do
|
|
||||||
cRoute <- getCurrentRoute
|
|
||||||
case cRoute of
|
|
||||||
Just (CTutorialR tid csh ssh tutn TInviteR) ->
|
|
||||||
fetchTutorialId tid csh ssh tutn
|
|
||||||
_other ->
|
|
||||||
error "tutorInvitationConfig called from unsupported route"
|
|
||||||
invitationSubject (Entity _ Tutorial{..}) _ = do
|
|
||||||
Course{..} <- get404 tutorialCourse
|
|
||||||
return . SomeMessage $ MsgMailSubjectTutorInvitation courseTerm courseSchool courseShorthand tutorialName
|
|
||||||
invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName
|
|
||||||
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|]
|
|
||||||
invitationTokenConfig _ _ = do
|
|
||||||
itAuthority <- liftHandler requireAuthId
|
|
||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
|
||||||
invitationRestriction _ _ = return Authorized
|
|
||||||
invitationForm _ _ _ = pure (JunctionTutor, ())
|
|
||||||
invitationInsertHook _ _ _ _ = id
|
|
||||||
invitationSuccessMsg (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted tutorialName
|
|
||||||
invitationUltDest (Entity _ Tutorial{..}) _ = do
|
|
||||||
Course{..} <- get404 tutorialCourse
|
|
||||||
return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CTutorialListR
|
|
||||||
|
|
||||||
getTInviteR, postTInviteR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
|
||||||
getTInviteR = postTInviteR
|
|
||||||
postTInviteR = invitationR tutorInvitationConfig
|
|
||||||
|
|
||||||
|
|
||||||
data TutorialForm = TutorialForm
|
|
||||||
{ tfName :: TutorialName
|
|
||||||
, tfType :: CI Text
|
|
||||||
, tfCapacity :: Maybe Int
|
|
||||||
, tfRoom :: Text
|
|
||||||
, tfTime :: Occurrences
|
|
||||||
, tfRegGroup :: Maybe (CI Text)
|
|
||||||
, tfRegisterFrom :: Maybe UTCTime
|
|
||||||
, tfRegisterTo :: Maybe UTCTime
|
|
||||||
, tfDeregisterUntil :: Maybe UTCTime
|
|
||||||
, tfTutors :: Set (Either UserEmail UserId)
|
|
||||||
}
|
|
||||||
|
|
||||||
tutorialForm :: CourseId -> Maybe TutorialForm -> Form TutorialForm
|
|
||||||
tutorialForm cid template html = do
|
|
||||||
MsgRenderer mr <- getMsgRenderer
|
|
||||||
cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute
|
|
||||||
uid <- liftHandler requireAuthId
|
|
||||||
|
|
||||||
let
|
|
||||||
tutorForm = Set.fromList <$> massInputAccumA miAdd' miCell' (\p -> Just . SomeRoute $ cRoute :#: p) miLayout' ("tutors" :: Text) (fslI MsgTutorialTutors & setTooltip MsgMassInputTip) True (Set.toList . tfTutors <$> template)
|
|
||||||
where
|
|
||||||
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
|
|
||||||
miAdd' nudge submitView csrf = do
|
|
||||||
(addRes, addView) <- mpreq (multiUserField False . Just $ tutUserSuggestions uid) ("" & addName (nudge "email")) Nothing
|
|
||||||
let
|
|
||||||
addRes'
|
|
||||||
| otherwise
|
|
||||||
= addRes <&> \newDat oldDat -> if
|
|
||||||
| existing <- newDat `Set.intersection` Set.fromList oldDat
|
|
||||||
, not $ Set.null existing
|
|
||||||
-> FormFailure [mr MsgTutorialTutorAlreadyAdded]
|
|
||||||
| otherwise
|
|
||||||
-> FormSuccess $ Set.toList newDat
|
|
||||||
return (addRes', $(widgetFile "tutorial/tutorMassInput/add"))
|
|
||||||
|
|
||||||
|
|
||||||
miCell' :: Either UserEmail UserId -> Widget
|
|
||||||
miCell' (Left email) =
|
|
||||||
$(widgetFile "tutorial/tutorMassInput/cellInvitation")
|
|
||||||
miCell' (Right userId) = do
|
|
||||||
User{..} <- liftHandler . runDB $ get404 userId
|
|
||||||
$(widgetFile "tutorial/tutorMassInput/cellKnown")
|
|
||||||
|
|
||||||
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
|
|
||||||
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "tutorial/tutorMassInput/layout")
|
|
||||||
|
|
||||||
flip (renderAForm FormStandard) html $ TutorialForm
|
|
||||||
<$> areq (textField & cfStrip & cfCI) (fslpI MsgTutorialName (mr MsgTutorialName) & setTooltip MsgTutorialNameTip) (tfName <$> template)
|
|
||||||
<*> areq (textField & cfStrip & cfCI & addDatalist tutTypeDatalist) (fslpI MsgTutorialType $ mr MsgTutorialType) (tfType <$> template)
|
|
||||||
<*> aopt (natFieldI MsgTutorialCapacityNonPositive) (fslpI MsgTutorialCapacity (mr MsgTutorialCapacity) & setTooltip MsgTutorialCapacityTip) (tfCapacity <$> template)
|
|
||||||
<*> areq textField (fslpI MsgTutorialRoom $ mr MsgTutorialRoomPlaceholder) (tfRoom <$> template)
|
|
||||||
<*> occurrencesAForm ("occurrences" :: Text) (tfTime <$> template)
|
|
||||||
<*> aopt (textField & cfStrip & cfCI) (fslI MsgTutorialRegGroup & setTooltip MsgTutorialRegGroupTip) ((tfRegGroup <$> template) <|> Just (Just "tutorial"))
|
|
||||||
<*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
|
|
||||||
& setTooltip MsgCourseRegisterFromTip
|
|
||||||
) (tfRegisterFrom <$> template)
|
|
||||||
<*> aopt utcTimeField (fslpI MsgRegisterTo (mr MsgDate)
|
|
||||||
& setTooltip MsgCourseRegisterToTip
|
|
||||||
) (tfRegisterTo <$> template)
|
|
||||||
<*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
|
|
||||||
& setTooltip MsgCourseDeregisterUntilTip
|
|
||||||
) (tfDeregisterUntil <$> template)
|
|
||||||
<*> tutorForm
|
|
||||||
where
|
|
||||||
tutTypeDatalist :: HandlerFor UniWorX (OptionList (CI Text))
|
|
||||||
tutTypeDatalist = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $
|
|
||||||
fmap (setOf $ folded . _Value) . E.select . E.from $ \tutorial -> do
|
|
||||||
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
|
||||||
return $ tutorial E.^. TutorialType
|
|
||||||
|
|
||||||
tutUserSuggestions :: UserId -> E.SqlQuery (E.SqlExpr (Entity User))
|
|
||||||
tutUserSuggestions uid = E.from $ \(lecturer `E.InnerJoin` course `E.InnerJoin` tutorial `E.InnerJoin` tutor `E.InnerJoin` tutorUser) -> do
|
|
||||||
E.on $ tutorUser E.^. UserId E.==. tutor E.^. TutorUser
|
|
||||||
E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
|
|
||||||
E.on $ tutorial E.^. TutorialCourse E.==. course E.^. CourseId
|
|
||||||
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
|
|
||||||
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
|
||||||
return tutorUser
|
|
||||||
|
|
||||||
|
|
||||||
getCTutorialNewR, postCTutorialNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
|
||||||
getCTutorialNewR = postCTutorialNewR
|
|
||||||
postCTutorialNewR tid ssh csh = do
|
|
||||||
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
|
|
||||||
|
|
||||||
((newTutResult, newTutWidget), newTutEnctype) <- runFormPost $ tutorialForm cid Nothing
|
|
||||||
|
|
||||||
formResult newTutResult $ \TutorialForm{..} -> do
|
|
||||||
insertRes <- runDBJobs $ do
|
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
insertRes <- insertUnique Tutorial
|
|
||||||
{ tutorialName = tfName
|
|
||||||
, tutorialCourse = cid
|
|
||||||
, tutorialType = tfType
|
|
||||||
, tutorialCapacity = tfCapacity
|
|
||||||
, tutorialRoom = tfRoom
|
|
||||||
, tutorialTime = tfTime
|
|
||||||
, tutorialRegGroup = tfRegGroup
|
|
||||||
, tutorialRegisterFrom = tfRegisterFrom
|
|
||||||
, tutorialRegisterTo = tfRegisterTo
|
|
||||||
, tutorialDeregisterUntil = tfDeregisterUntil
|
|
||||||
, tutorialLastChanged = now
|
|
||||||
}
|
|
||||||
whenIsJust insertRes $ \tutid -> do
|
|
||||||
let (invites, adds) = partitionEithers $ Set.toList tfTutors
|
|
||||||
insertMany_ $ map (Tutor tutid) adds
|
|
||||||
sinkInvitationsF tutorInvitationConfig $ map (, tutid, (InvDBDataTutor, InvTokenDataTutor)) invites
|
|
||||||
return insertRes
|
|
||||||
case insertRes of
|
|
||||||
Nothing -> addMessageI Error $ MsgTutorialNameTaken tfName
|
|
||||||
Just _ -> do
|
|
||||||
addMessageI Success $ MsgTutorialCreated tfName
|
|
||||||
redirect $ CourseR tid ssh csh CTutorialListR
|
|
||||||
|
|
||||||
let heading = prependCourseTitle tid ssh csh MsgTutorialNew
|
|
||||||
|
|
||||||
siteLayoutMsg heading $ do
|
|
||||||
setTitleI heading
|
|
||||||
let
|
|
||||||
newTutForm = wrapForm newTutWidget def
|
|
||||||
{ formMethod = POST
|
|
||||||
, formAction = Just . SomeRoute $ CourseR tid ssh csh CTutorialNewR
|
|
||||||
, formEncoding = newTutEnctype
|
|
||||||
}
|
|
||||||
$(widgetFile "tutorial-new")
|
|
||||||
|
|
||||||
getTEditR, postTEditR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
|
||||||
getTEditR = postTEditR
|
|
||||||
postTEditR tid ssh csh tutn = do
|
|
||||||
(cid, tutid, template) <- runDB $ do
|
|
||||||
(cid, Entity tutid Tutorial{..}) <- fetchCourseIdTutorial tid ssh csh tutn
|
|
||||||
|
|
||||||
tutorIds <- fmap (map E.unValue) . E.select . E.from $ \tutor -> do
|
|
||||||
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
|
|
||||||
return $ tutor E.^. TutorUser
|
|
||||||
|
|
||||||
tutorInvites <- sourceInvitationsF @Tutor tutid
|
|
||||||
|
|
||||||
let
|
|
||||||
template = TutorialForm
|
|
||||||
{ tfName = tutorialName
|
|
||||||
, tfType = tutorialType
|
|
||||||
, tfCapacity = tutorialCapacity
|
|
||||||
, tfRoom = tutorialRoom
|
|
||||||
, tfTime = tutorialTime
|
|
||||||
, tfRegGroup = tutorialRegGroup
|
|
||||||
, tfRegisterFrom = tutorialRegisterFrom
|
|
||||||
, tfRegisterTo = tutorialRegisterTo
|
|
||||||
, tfDeregisterUntil = tutorialDeregisterUntil
|
|
||||||
, tfTutors = Set.fromList (map Right tutorIds)
|
|
||||||
<> Set.mapMonotonic Left (Map.keysSet tutorInvites)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (cid, tutid, template)
|
|
||||||
|
|
||||||
((newTutResult, newTutWidget), newTutEnctype) <- runFormPost . tutorialForm cid $ Just template
|
|
||||||
|
|
||||||
formResult newTutResult $ \TutorialForm{..} -> do
|
|
||||||
insertRes <- runDBJobs $ do
|
|
||||||
now <- liftIO getCurrentTime
|
|
||||||
insertRes <- myReplaceUnique tutid Tutorial
|
|
||||||
{ tutorialName = tfName
|
|
||||||
, tutorialCourse = cid
|
|
||||||
, tutorialType = tfType
|
|
||||||
, tutorialCapacity = tfCapacity
|
|
||||||
, tutorialRoom = tfRoom
|
|
||||||
, tutorialTime = tfTime
|
|
||||||
, tutorialRegGroup = tfRegGroup
|
|
||||||
, tutorialRegisterFrom = tfRegisterFrom
|
|
||||||
, tutorialRegisterTo = tfRegisterTo
|
|
||||||
, tutorialDeregisterUntil = tfDeregisterUntil
|
|
||||||
, tutorialLastChanged = now
|
|
||||||
}
|
|
||||||
when (is _Nothing insertRes) $ do
|
|
||||||
let (invites, adds) = partitionEithers $ Set.toList tfTutors
|
|
||||||
|
|
||||||
deleteWhere [ TutorTutorial ==. tutid ]
|
|
||||||
insertMany_ $ map (Tutor tutid) adds
|
|
||||||
|
|
||||||
deleteWhere [ InvitationFor ==. invRef @Tutor tutid, InvitationEmail /<-. invites ]
|
|
||||||
sinkInvitationsF tutorInvitationConfig $ map (, tutid, (InvDBDataTutor, InvTokenDataTutor)) invites
|
|
||||||
return insertRes
|
|
||||||
case insertRes of
|
|
||||||
Just _ -> addMessageI Error $ MsgTutorialNameTaken tfName
|
|
||||||
Nothing -> do
|
|
||||||
addMessageI Success $ MsgTutorialEdited tfName
|
|
||||||
redirect $ CourseR tid ssh csh CTutorialListR
|
|
||||||
|
|
||||||
let heading = prependCourseTitle tid ssh csh . MsgTutorialEditHeading $ tfName template
|
|
||||||
|
|
||||||
siteLayoutMsg heading $ do
|
|
||||||
setTitleI heading
|
|
||||||
let
|
|
||||||
newTutForm = wrapForm newTutWidget def
|
|
||||||
{ formMethod = POST
|
|
||||||
, formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutn TEditR
|
|
||||||
, formEncoding = newTutEnctype
|
|
||||||
}
|
|
||||||
$(widgetFile "tutorial-edit")
|
|
||||||
|
|||||||
81
src/Handler/Tutorial/Communication.hs
Normal file
81
src/Handler/Tutorial/Communication.hs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
module Handler.Tutorial.Communication
|
||||||
|
( getTCommR, postTCommR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Tutorial
|
||||||
|
import Handler.Utils.Communication
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
|
getTCommR, postTCommR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
||||||
|
getTCommR = postTCommR
|
||||||
|
postTCommR tid ssh csh tutn = do
|
||||||
|
jSender <- requireAuthId
|
||||||
|
(cid, tutid) <- runDB $ fetchCourseIdTutorialId tid ssh csh tutn
|
||||||
|
|
||||||
|
commR CommunicationRoute
|
||||||
|
{ crHeading = SomeMessage . prependCourseTitle tid ssh csh $ SomeMessage MsgCommTutorialHeading
|
||||||
|
, crUltDest = SomeRoute $ CTutorialR tid ssh csh tutn TCommR
|
||||||
|
, crJobs = \Communication{..} -> do
|
||||||
|
let jSubject = cSubject
|
||||||
|
jMailContent = cBody
|
||||||
|
jCourse = cid
|
||||||
|
allRecipients = Set.toList $ Set.insert (Right jSender) cRecipients
|
||||||
|
jMailObjectUUID <- liftIO getRandom
|
||||||
|
jAllRecipientAddresses <- lift . fmap Set.fromList . forM allRecipients $ \case
|
||||||
|
Left email -> return . Address Nothing $ CI.original email
|
||||||
|
Right rid -> userAddress <$> getJust rid
|
||||||
|
forM_ allRecipients $ \jRecipientEmail ->
|
||||||
|
yield JobSendCourseCommunication{..}
|
||||||
|
, crRecipients = Map.fromList
|
||||||
|
[ ( RGTutorialParticipants
|
||||||
|
, E.from $ \(user `E.InnerJoin` participant) -> do
|
||||||
|
E.on $ user E.^. UserId E.==. participant E.^. TutorialParticipantUser
|
||||||
|
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. E.val tutid
|
||||||
|
return user
|
||||||
|
)
|
||||||
|
, ( RGCourseLecturers
|
||||||
|
, E.from $ \(user `E.InnerJoin` lecturer) -> do
|
||||||
|
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
||||||
|
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
||||||
|
return user
|
||||||
|
)
|
||||||
|
, ( RGCourseCorrectors
|
||||||
|
, E.from $ \user -> do
|
||||||
|
E.where_ $ E.exists $ E.from $ \(sheet `E.InnerJoin` corrector) -> do
|
||||||
|
E.on $ sheet E.^. SheetId E.==. corrector E.^. SheetCorrectorSheet
|
||||||
|
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
|
||||||
|
E.&&. corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
||||||
|
return user
|
||||||
|
)
|
||||||
|
, ( RGCourseTutors
|
||||||
|
, E.from $ \user -> do
|
||||||
|
E.where_ $ E.exists $ E.from $ \(tutorial `E.InnerJoin` tutor) -> do
|
||||||
|
E.on $ tutorial E.^. TutorialId E.==. tutor E.^. TutorTutorial
|
||||||
|
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
||||||
|
E.&&. tutor E.^. TutorUser E.==. user E.^. UserId
|
||||||
|
return user
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, crRecipientAuth = Just $ \uid -> do
|
||||||
|
isTutorialUser <- E.selectExists . E.from $ \tutorialUser ->
|
||||||
|
E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid
|
||||||
|
E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid
|
||||||
|
|
||||||
|
isAssociatedCorrector <- evalAccessForDB (Just uid) (CourseR tid ssh csh CNotesR) False
|
||||||
|
isAssociatedTutor <- evalAccessForDB (Just uid) (CourseR tid ssh csh CTutorialListR) False
|
||||||
|
|
||||||
|
mr <- getMsgRenderer
|
||||||
|
return $ if
|
||||||
|
| isTutorialUser -> Authorized
|
||||||
|
| otherwise -> orAR mr isAssociatedCorrector isAssociatedTutor
|
||||||
|
}
|
||||||
39
src/Handler/Tutorial/Delete.hs
Normal file
39
src/Handler/Tutorial/Delete.hs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
module Handler.Tutorial.Delete
|
||||||
|
( getTDeleteR, postTDeleteR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Tutorial
|
||||||
|
import Handler.Utils.Delete
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
|
getTDeleteR, postTDeleteR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
||||||
|
getTDeleteR = postTDeleteR
|
||||||
|
postTDeleteR tid ssh csh tutn = do
|
||||||
|
tutid <- runDB $ fetchTutorialId tid ssh csh tutn
|
||||||
|
deleteR DeleteRoute
|
||||||
|
{ drRecords = Set.singleton tutid
|
||||||
|
, drUnjoin = \(_ `E.InnerJoin` tutorial) -> tutorial
|
||||||
|
, drGetInfo = \(course `E.InnerJoin` tutorial) -> do
|
||||||
|
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
|
||||||
|
let participants = E.sub_select . E.from $ \participant -> do
|
||||||
|
E.where_ $ participant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
||||||
|
return E.countRows
|
||||||
|
return (course, tutorial, participants :: E.SqlExpr (E.Value Int))
|
||||||
|
, drRenderRecord = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
|
||||||
|
return [whamlet|_{prependCourseTitle courseTerm courseSchool courseShorthand (CI.original tutorialName)} (_{MsgParticipantsN ps})|]
|
||||||
|
, drRecordConfirmString = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
|
||||||
|
return [st|#{termToText (unTermKey courseTerm)}/#{unSchoolKey courseSchool}/#{courseShorthand}/#{tutorialName}+#{tshow ps}|]
|
||||||
|
, drCaption = SomeMessage MsgTutorialDeleteQuestion
|
||||||
|
, drSuccessMessage = SomeMessage MsgTutorialDeleted
|
||||||
|
, drAbort = SomeRoute $ CTutorialR tid ssh csh tutn TUsersR
|
||||||
|
, drSuccess = SomeRoute $ CourseR tid ssh csh CTutorialListR
|
||||||
|
, drDelete = const id -- TODO: audit
|
||||||
|
}
|
||||||
92
src/Handler/Tutorial/Edit.hs
Normal file
92
src/Handler/Tutorial/Edit.hs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
module Handler.Tutorial.Edit
|
||||||
|
( getTEditR, postTEditR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Tutorial
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Handler.Tutorial.Form
|
||||||
|
import Handler.Tutorial.TutorInvite
|
||||||
|
|
||||||
|
|
||||||
|
getTEditR, postTEditR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
||||||
|
getTEditR = postTEditR
|
||||||
|
postTEditR tid ssh csh tutn = do
|
||||||
|
(cid, tutid, template) <- runDB $ do
|
||||||
|
(cid, Entity tutid Tutorial{..}) <- fetchCourseIdTutorial tid ssh csh tutn
|
||||||
|
|
||||||
|
tutorIds <- fmap (map E.unValue) . E.select . E.from $ \tutor -> do
|
||||||
|
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
|
||||||
|
return $ tutor E.^. TutorUser
|
||||||
|
|
||||||
|
tutorInvites <- sourceInvitationsF @Tutor tutid
|
||||||
|
|
||||||
|
let
|
||||||
|
template = TutorialForm
|
||||||
|
{ tfName = tutorialName
|
||||||
|
, tfType = tutorialType
|
||||||
|
, tfCapacity = tutorialCapacity
|
||||||
|
, tfRoom = tutorialRoom
|
||||||
|
, tfTime = tutorialTime
|
||||||
|
, tfRegGroup = tutorialRegGroup
|
||||||
|
, tfRegisterFrom = tutorialRegisterFrom
|
||||||
|
, tfRegisterTo = tutorialRegisterTo
|
||||||
|
, tfDeregisterUntil = tutorialDeregisterUntil
|
||||||
|
, tfTutors = Set.fromList (map Right tutorIds)
|
||||||
|
<> Set.mapMonotonic Left (Map.keysSet tutorInvites)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cid, tutid, template)
|
||||||
|
|
||||||
|
((newTutResult, newTutWidget), newTutEnctype) <- runFormPost . tutorialForm cid $ Just template
|
||||||
|
|
||||||
|
formResult newTutResult $ \TutorialForm{..} -> do
|
||||||
|
insertRes <- runDBJobs $ do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
insertRes <- myReplaceUnique tutid Tutorial
|
||||||
|
{ tutorialName = tfName
|
||||||
|
, tutorialCourse = cid
|
||||||
|
, tutorialType = tfType
|
||||||
|
, tutorialCapacity = tfCapacity
|
||||||
|
, tutorialRoom = tfRoom
|
||||||
|
, tutorialTime = tfTime
|
||||||
|
, tutorialRegGroup = tfRegGroup
|
||||||
|
, tutorialRegisterFrom = tfRegisterFrom
|
||||||
|
, tutorialRegisterTo = tfRegisterTo
|
||||||
|
, tutorialDeregisterUntil = tfDeregisterUntil
|
||||||
|
, tutorialLastChanged = now
|
||||||
|
}
|
||||||
|
when (is _Nothing insertRes) $ do
|
||||||
|
let (invites, adds) = partitionEithers $ Set.toList tfTutors
|
||||||
|
|
||||||
|
deleteWhere [ TutorTutorial ==. tutid ]
|
||||||
|
insertMany_ $ map (Tutor tutid) adds
|
||||||
|
|
||||||
|
deleteWhere [ InvitationFor ==. invRef @Tutor tutid, InvitationEmail /<-. invites ]
|
||||||
|
sinkInvitationsF tutorInvitationConfig $ map (, tutid, (InvDBDataTutor, InvTokenDataTutor)) invites
|
||||||
|
return insertRes
|
||||||
|
case insertRes of
|
||||||
|
Just _ -> addMessageI Error $ MsgTutorialNameTaken tfName
|
||||||
|
Nothing -> do
|
||||||
|
addMessageI Success $ MsgTutorialEdited tfName
|
||||||
|
redirect $ CourseR tid ssh csh CTutorialListR
|
||||||
|
|
||||||
|
let heading = prependCourseTitle tid ssh csh . MsgTutorialEditHeading $ tfName template
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
let
|
||||||
|
newTutForm = wrapForm newTutWidget def
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just . SomeRoute $ CTutorialR tid ssh csh tutn TEditR
|
||||||
|
, formEncoding = newTutEnctype
|
||||||
|
}
|
||||||
|
$(widgetFile "tutorial-edit")
|
||||||
96
src/Handler/Tutorial/Form.hs
Normal file
96
src/Handler/Tutorial/Form.hs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
module Handler.Tutorial.Form
|
||||||
|
( TutorialForm(..)
|
||||||
|
, tutorialForm
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Form.Occurrences
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import Data.Map ((!))
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
|
data TutorialForm = TutorialForm
|
||||||
|
{ tfName :: TutorialName
|
||||||
|
, tfType :: CI Text
|
||||||
|
, tfCapacity :: Maybe Int
|
||||||
|
, tfRoom :: Text
|
||||||
|
, tfTime :: Occurrences
|
||||||
|
, tfRegGroup :: Maybe (CI Text)
|
||||||
|
, tfRegisterFrom :: Maybe UTCTime
|
||||||
|
, tfRegisterTo :: Maybe UTCTime
|
||||||
|
, tfDeregisterUntil :: Maybe UTCTime
|
||||||
|
, tfTutors :: Set (Either UserEmail UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
tutorialForm :: CourseId -> Maybe TutorialForm -> Form TutorialForm
|
||||||
|
tutorialForm cid template html = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute
|
||||||
|
uid <- liftHandler requireAuthId
|
||||||
|
|
||||||
|
let
|
||||||
|
tutorForm = Set.fromList <$> massInputAccumA miAdd' miCell' (\p -> Just . SomeRoute $ cRoute :#: p) miLayout' ("tutors" :: Text) (fslI MsgTutorialTutors & setTooltip MsgMassInputTip) True (Set.toList . tfTutors <$> template)
|
||||||
|
where
|
||||||
|
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
|
||||||
|
miAdd' nudge submitView csrf = do
|
||||||
|
(addRes, addView) <- mpreq (multiUserField False . Just $ tutUserSuggestions uid) ("" & addName (nudge "email")) Nothing
|
||||||
|
let
|
||||||
|
addRes'
|
||||||
|
| otherwise
|
||||||
|
= addRes <&> \newDat oldDat -> if
|
||||||
|
| existing <- newDat `Set.intersection` Set.fromList oldDat
|
||||||
|
, not $ Set.null existing
|
||||||
|
-> FormFailure [mr MsgTutorialTutorAlreadyAdded]
|
||||||
|
| otherwise
|
||||||
|
-> FormSuccess $ Set.toList newDat
|
||||||
|
return (addRes', $(widgetFile "tutorial/tutorMassInput/add"))
|
||||||
|
|
||||||
|
|
||||||
|
miCell' :: Either UserEmail UserId -> Widget
|
||||||
|
miCell' (Left email) =
|
||||||
|
$(widgetFile "tutorial/tutorMassInput/cellInvitation")
|
||||||
|
miCell' (Right userId) = do
|
||||||
|
User{..} <- liftHandler . runDB $ get404 userId
|
||||||
|
$(widgetFile "tutorial/tutorMassInput/cellKnown")
|
||||||
|
|
||||||
|
miLayout' :: MassInputLayout ListLength (Either UserEmail UserId) ()
|
||||||
|
miLayout' lLength _ cellWdgts delButtons addWdgts = $(widgetFile "tutorial/tutorMassInput/layout")
|
||||||
|
|
||||||
|
flip (renderAForm FormStandard) html $ TutorialForm
|
||||||
|
<$> areq (textField & cfStrip & cfCI) (fslpI MsgTutorialName (mr MsgTutorialName) & setTooltip MsgTutorialNameTip) (tfName <$> template)
|
||||||
|
<*> areq (textField & cfStrip & cfCI & addDatalist tutTypeDatalist) (fslpI MsgTutorialType $ mr MsgTutorialType) (tfType <$> template)
|
||||||
|
<*> aopt (natFieldI MsgTutorialCapacityNonPositive) (fslpI MsgTutorialCapacity (mr MsgTutorialCapacity) & setTooltip MsgTutorialCapacityTip) (tfCapacity <$> template)
|
||||||
|
<*> areq textField (fslpI MsgTutorialRoom $ mr MsgTutorialRoomPlaceholder) (tfRoom <$> template)
|
||||||
|
<*> occurrencesAForm ("occurrences" :: Text) (tfTime <$> template)
|
||||||
|
<*> aopt (textField & cfStrip & cfCI) (fslI MsgTutorialRegGroup & setTooltip MsgTutorialRegGroupTip) ((tfRegGroup <$> template) <|> Just (Just "tutorial"))
|
||||||
|
<*> aopt utcTimeField (fslpI MsgRegisterFrom (mr MsgDate)
|
||||||
|
& setTooltip MsgCourseRegisterFromTip
|
||||||
|
) (tfRegisterFrom <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgRegisterTo (mr MsgDate)
|
||||||
|
& setTooltip MsgCourseRegisterToTip
|
||||||
|
) (tfRegisterTo <$> template)
|
||||||
|
<*> aopt utcTimeField (fslpI MsgDeRegUntil (mr MsgDate)
|
||||||
|
& setTooltip MsgCourseDeregisterUntilTip
|
||||||
|
) (tfDeregisterUntil <$> template)
|
||||||
|
<*> tutorForm
|
||||||
|
where
|
||||||
|
tutTypeDatalist :: HandlerFor UniWorX (OptionList (CI Text))
|
||||||
|
tutTypeDatalist = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $
|
||||||
|
fmap (setOf $ folded . _Value) . E.select . E.from $ \tutorial -> do
|
||||||
|
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
||||||
|
return $ tutorial E.^. TutorialType
|
||||||
|
|
||||||
|
tutUserSuggestions :: UserId -> E.SqlQuery (E.SqlExpr (Entity User))
|
||||||
|
tutUserSuggestions uid = E.from $ \(lecturer `E.InnerJoin` course `E.InnerJoin` tutorial `E.InnerJoin` tutor `E.InnerJoin` tutorUser) -> do
|
||||||
|
E.on $ tutorUser E.^. UserId E.==. tutor E.^. TutorUser
|
||||||
|
E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
|
||||||
|
E.on $ tutorial E.^. TutorialCourse E.==. course E.^. CourseId
|
||||||
|
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
|
||||||
|
E.where_ $ lecturer E.^. LecturerUser E.==. E.val uid
|
||||||
|
return tutorUser
|
||||||
87
src/Handler/Tutorial/List.hs
Normal file
87
src/Handler/Tutorial/List.hs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
module Handler.Tutorial.List
|
||||||
|
( getCTutorialListR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import Database.Esqueleto.Utils.TH
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
|
getCTutorialListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
getCTutorialListR tid ssh csh = do
|
||||||
|
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
|
||||||
|
let
|
||||||
|
tutorialDBTable = DBTable{..}
|
||||||
|
where
|
||||||
|
dbtSQLQuery tutorial = do
|
||||||
|
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
||||||
|
let participants = E.sub_select . E.from $ \tutorialParticipant -> do
|
||||||
|
E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
||||||
|
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
|
||||||
|
return (tutorial, participants)
|
||||||
|
dbtRowKey = (E.^. TutorialId)
|
||||||
|
dbtProj = return . over (_dbrOutput . _2) E.unValue
|
||||||
|
dbtColonnade = dbColonnade $ mconcat
|
||||||
|
[ sortable (Just "type") (i18nCell MsgTutorialType) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell $ CI.original tutorialType
|
||||||
|
, sortable (Just "name") (i18nCell MsgTutorialName) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) [whamlet|#{tutorialName}|]
|
||||||
|
, sortable Nothing (i18nCell MsgTutorialTutors) $ \DBRow{ dbrOutput = (Entity tutid _, _) } -> sqlCell $ do
|
||||||
|
tutors <- fmap (map $(unValueN 3)) . E.select . E.from $ \(tutor `E.InnerJoin` user) -> do
|
||||||
|
E.on $ tutor E.^. TutorUser E.==. user E.^. UserId
|
||||||
|
E.where_ $ tutor E.^. TutorTutorial E.==. E.val tutid
|
||||||
|
return (user E.^. UserEmail, user E.^. UserDisplayName, user E.^. UserSurname)
|
||||||
|
return [whamlet|
|
||||||
|
$newline never
|
||||||
|
<ul .list--iconless .list--inline .list--comma-separated>
|
||||||
|
$forall tutor <- tutors
|
||||||
|
<li>
|
||||||
|
^{nameEmailWidget' tutor}
|
||||||
|
|]
|
||||||
|
, sortable (Just "participants") (i18nCell MsgTutorialParticipants) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, n) } -> anchorCell (CTutorialR tid ssh csh tutorialName TUsersR) $ tshow n
|
||||||
|
, sortable (Just "capacity") (i18nCell MsgTutorialCapacity) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . tshow) tutorialCapacity
|
||||||
|
, sortable (Just "room") (i18nCell MsgTutorialRoom) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> textCell tutorialRoom
|
||||||
|
, sortable Nothing (i18nCell MsgTutorialTime) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> occurrencesCell tutorialTime
|
||||||
|
, sortable (Just "register-group") (i18nCell MsgTutorialRegGroup) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybe mempty (textCell . CI.original) tutorialRegGroup
|
||||||
|
, 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 "deregister-until") (i18nCell MsgTutorialDeregisterUntil) $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> maybeDateTimeCell tutorialDeregisterUntil
|
||||||
|
, sortable Nothing mempty $ \DBRow{ dbrOutput = (Entity _ Tutorial{..}, _) } -> cell $ do
|
||||||
|
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
|
||||||
|
]
|
||||||
|
dbtSorting = Map.fromList
|
||||||
|
[ ("type", SortColumn $ \tutorial -> tutorial E.^. TutorialType )
|
||||||
|
, ("name", SortColumn $ \tutorial -> tutorial E.^. TutorialName )
|
||||||
|
, ("participants", SortColumn $ \tutorial -> E.sub_select . E.from $ \tutorialParticipant -> do
|
||||||
|
E.where_ $ tutorialParticipant E.^. TutorialParticipantTutorial E.==. tutorial E.^. TutorialId
|
||||||
|
return E.countRows :: E.SqlQuery (E.SqlExpr (E.Value Int))
|
||||||
|
)
|
||||||
|
, ("capacity", SortColumn $ \tutorial -> tutorial E.^. TutorialCapacity )
|
||||||
|
, ("room", SortColumn $ \tutorial -> tutorial E.^. TutorialRoom )
|
||||||
|
, ("register-group", SortColumn $ \tutorial -> tutorial E.^. TutorialRegGroup )
|
||||||
|
, ("register-from", SortColumn $ \tutorial -> tutorial E.^. TutorialRegisterFrom )
|
||||||
|
, ("register-to", SortColumn $ \tutorial -> tutorial E.^. TutorialRegisterTo )
|
||||||
|
, ("deregister-until", SortColumn $ \tutorial -> tutorial E.^. TutorialDeregisterUntil )
|
||||||
|
]
|
||||||
|
dbtFilter = Map.empty
|
||||||
|
dbtFilterUI = const mempty
|
||||||
|
dbtStyle = def
|
||||||
|
dbtParams = def
|
||||||
|
dbtIdent :: Text
|
||||||
|
dbtIdent = "tutorials"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
|
tutorialDBTableValidator = def
|
||||||
|
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
||||||
|
((), tutorialTable) <- runDB $ dbTable tutorialDBTableValidator tutorialDBTable
|
||||||
|
|
||||||
|
siteLayoutMsg (prependCourseTitle tid ssh csh MsgTutorialsHeading) $ do
|
||||||
|
setTitleI $ prependCourseTitle tid ssh csh MsgTutorialsHeading
|
||||||
|
$(widgetFile "tutorial-list")
|
||||||
60
src/Handler/Tutorial/New.hs
Normal file
60
src/Handler/Tutorial/New.hs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
module Handler.Tutorial.New
|
||||||
|
( getCTutorialNewR, postCTutorialNewR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Handler.Tutorial.Form
|
||||||
|
import Handler.Tutorial.TutorInvite
|
||||||
|
|
||||||
|
|
||||||
|
getCTutorialNewR, postCTutorialNewR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
|
getCTutorialNewR = postCTutorialNewR
|
||||||
|
postCTutorialNewR tid ssh csh = do
|
||||||
|
Entity cid Course{..} <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
|
||||||
|
|
||||||
|
((newTutResult, newTutWidget), newTutEnctype) <- runFormPost $ tutorialForm cid Nothing
|
||||||
|
|
||||||
|
formResult newTutResult $ \TutorialForm{..} -> do
|
||||||
|
insertRes <- runDBJobs $ do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
insertRes <- insertUnique Tutorial
|
||||||
|
{ tutorialName = tfName
|
||||||
|
, tutorialCourse = cid
|
||||||
|
, tutorialType = tfType
|
||||||
|
, tutorialCapacity = tfCapacity
|
||||||
|
, tutorialRoom = tfRoom
|
||||||
|
, tutorialTime = tfTime
|
||||||
|
, tutorialRegGroup = tfRegGroup
|
||||||
|
, tutorialRegisterFrom = tfRegisterFrom
|
||||||
|
, tutorialRegisterTo = tfRegisterTo
|
||||||
|
, tutorialDeregisterUntil = tfDeregisterUntil
|
||||||
|
, tutorialLastChanged = now
|
||||||
|
}
|
||||||
|
whenIsJust insertRes $ \tutid -> do
|
||||||
|
let (invites, adds) = partitionEithers $ Set.toList tfTutors
|
||||||
|
insertMany_ $ map (Tutor tutid) adds
|
||||||
|
sinkInvitationsF tutorInvitationConfig $ map (, tutid, (InvDBDataTutor, InvTokenDataTutor)) invites
|
||||||
|
return insertRes
|
||||||
|
case insertRes of
|
||||||
|
Nothing -> addMessageI Error $ MsgTutorialNameTaken tfName
|
||||||
|
Just _ -> do
|
||||||
|
addMessageI Success $ MsgTutorialCreated tfName
|
||||||
|
redirect $ CourseR tid ssh csh CTutorialListR
|
||||||
|
|
||||||
|
let heading = prependCourseTitle tid ssh csh MsgTutorialNew
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
let
|
||||||
|
newTutForm = wrapForm newTutWidget def
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just . SomeRoute $ CourseR tid ssh csh CTutorialNewR
|
||||||
|
, formEncoding = newTutEnctype
|
||||||
|
}
|
||||||
|
$(widgetFile "tutorial-new")
|
||||||
28
src/Handler/Tutorial/Register.hs
Normal file
28
src/Handler/Tutorial/Register.hs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
module Handler.Tutorial.Register
|
||||||
|
( postTRegisterR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Tutorial
|
||||||
|
|
||||||
|
|
||||||
|
postTRegisterR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler ()
|
||||||
|
postTRegisterR tid ssh csh tutn = do
|
||||||
|
uid <- requireAuthId
|
||||||
|
|
||||||
|
Entity tutid Tutorial{..} <- runDB $ fetchTutorial tid ssh csh tutn
|
||||||
|
|
||||||
|
((btnResult, _), _) <- runFormPost buttonForm
|
||||||
|
|
||||||
|
formResult btnResult $ \case
|
||||||
|
BtnRegister -> do
|
||||||
|
runDB . void . insert $ TutorialParticipant tutid uid
|
||||||
|
addMessageI Success $ MsgTutorialRegisteredSuccess tutorialName
|
||||||
|
redirect $ CourseR tid ssh csh CShowR
|
||||||
|
BtnDeregister -> do
|
||||||
|
runDB . deleteBy $ UniqueTutorialParticipant tutid uid
|
||||||
|
addMessageI Success $ MsgTutorialDeregisteredSuccess tutorialName
|
||||||
|
redirect $ CourseR tid ssh csh CShowR
|
||||||
|
|
||||||
|
invalidArgs ["Register/Deregister button required"]
|
||||||
79
src/Handler/Tutorial/TutorInvite.hs
Normal file
79
src/Handler/Tutorial/TutorInvite.hs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Handler.Tutorial.TutorInvite
|
||||||
|
( getTInviteR, postTInviteR
|
||||||
|
, tutorInvitationConfig
|
||||||
|
, InvitableJunction(..), InvitationDBData(..), InvitationTokenData(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils.Tutorial
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
|
||||||
|
import Data.Aeson hiding (Result(..))
|
||||||
|
import Text.Hamlet (ihamlet)
|
||||||
|
|
||||||
|
|
||||||
|
instance IsInvitableJunction Tutor where
|
||||||
|
type InvitationFor Tutor = Tutorial
|
||||||
|
data InvitableJunction Tutor = JunctionTutor
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationDBData Tutor = InvDBDataTutor
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationTokenData Tutor = InvTokenDataTutor
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
_InvitableJunction = iso
|
||||||
|
(\Tutor{..} -> (tutorUser, tutorTutorial, JunctionTutor))
|
||||||
|
(\(tutorUser, tutorTutorial, JunctionTutor) -> Tutor{..})
|
||||||
|
|
||||||
|
instance ToJSON (InvitableJunction Tutor) where
|
||||||
|
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
instance FromJSON (InvitableJunction Tutor) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationDBData Tutor) where
|
||||||
|
toJSON = genericToJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
instance FromJSON (InvitationDBData Tutor) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelToPathPiece' 4 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationTokenData Tutor) where
|
||||||
|
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
instance FromJSON (InvitationTokenData Tutor) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 4 }
|
||||||
|
|
||||||
|
tutorInvitationConfig :: InvitationConfig Tutor
|
||||||
|
tutorInvitationConfig = InvitationConfig{..}
|
||||||
|
where
|
||||||
|
invitationRoute (Entity _ Tutorial{..}) _ = do
|
||||||
|
Course{..} <- get404 tutorialCourse
|
||||||
|
return $ CTutorialR courseTerm courseSchool courseShorthand tutorialName TInviteR
|
||||||
|
invitationResolveFor _ = do
|
||||||
|
cRoute <- getCurrentRoute
|
||||||
|
case cRoute of
|
||||||
|
Just (CTutorialR tid csh ssh tutn TInviteR) ->
|
||||||
|
fetchTutorialId tid csh ssh tutn
|
||||||
|
_other ->
|
||||||
|
error "tutorInvitationConfig called from unsupported route"
|
||||||
|
invitationSubject (Entity _ Tutorial{..}) _ = do
|
||||||
|
Course{..} <- get404 tutorialCourse
|
||||||
|
return . SomeMessage $ MsgMailSubjectTutorInvitation courseTerm courseSchool courseShorthand tutorialName
|
||||||
|
invitationHeading (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgTutorInviteHeading tutorialName
|
||||||
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgTutorInviteExplanation}|]
|
||||||
|
invitationTokenConfig _ _ = do
|
||||||
|
itAuthority <- liftHandler requireAuthId
|
||||||
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
|
invitationRestriction _ _ = return Authorized
|
||||||
|
invitationForm _ _ _ = pure (JunctionTutor, ())
|
||||||
|
invitationInsertHook _ _ _ _ = id
|
||||||
|
invitationSuccessMsg (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted tutorialName
|
||||||
|
invitationUltDest (Entity _ Tutorial{..}) _ = do
|
||||||
|
Course{..} <- get404 tutorialCourse
|
||||||
|
return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CTutorialListR
|
||||||
|
|
||||||
|
getTInviteR, postTInviteR :: TermId -> SchoolId -> CourseShorthand -> TutorialName -> Handler Html
|
||||||
|
getTInviteR = postTInviteR
|
||||||
|
postTInviteR = invitationR tutorInvitationConfig
|
||||||
@ -73,6 +73,7 @@ postAdminUserAddR = do
|
|||||||
, userWarningDays = userDefaultWarningDays
|
, userWarningDays = userDefaultWarningDays
|
||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userMailLanguages = def
|
, userMailLanguages = def
|
||||||
|
, userCsvOptions = def
|
||||||
, userTokensIssuedAfter = Nothing
|
, userTokensIssuedAfter = Nothing
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
|||||||
@ -150,9 +150,9 @@ commR CommunicationRoute{..} = do
|
|||||||
-> Map (EnumPosition RecipientCategory, ListPosition) (FieldView UniWorX)
|
-> Map (EnumPosition RecipientCategory, ListPosition) (FieldView UniWorX)
|
||||||
-> Map (Natural, (EnumPosition RecipientCategory, ListPosition)) Widget
|
-> Map (Natural, (EnumPosition RecipientCategory, ListPosition)) Widget
|
||||||
-> Widget
|
-> Widget
|
||||||
miLayout liveliness state cellWdgts _delButtons addWdgts = do
|
miLayout liveliness cState cellWdgts _delButtons addWdgts = do
|
||||||
checkedIdentBase <- newIdent
|
checkedIdentBase <- newIdent
|
||||||
let checkedCategories = Set.mapMonotonic (unEnumPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False state) $ Map.keysSet state
|
let checkedCategories = Set.mapMonotonic (unEnumPosition . fst) . Set.filter (\k' -> Map.foldrWithKey (\k (_, checkState) -> (||) $ k == k' && checkState /= FormSuccess False && (checkState /= FormMissing || maybe True snd (chosenRecipients' !? k))) False cState) $ Map.keysSet cState
|
||||||
checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c
|
checkedIdent c = checkedIdentBase <> "-" <> toPathPiece c
|
||||||
hasContent c = not (null $ categoryIndices c) || Map.member (1, (EnumPosition c, 0)) addWdgts
|
hasContent c = not (null $ categoryIndices c) || Map.member (1, (EnumPosition c, 0)) addWdgts
|
||||||
categoryIndices c = Set.filter ((== c) . unEnumPosition . fst) $ review liveCoords liveliness
|
categoryIndices c = Set.filter ((== c) . unEnumPosition . fst) $ review liveCoords liveliness
|
||||||
@ -165,10 +165,13 @@ commR CommunicationRoute{..} = do
|
|||||||
postProcess :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId)
|
postProcess :: Map (EnumPosition RecipientCategory, ListPosition) (Either UserEmail UserId, Bool) -> Set (Either UserEmail UserId)
|
||||||
postProcess = Set.fromList . map fst . filter snd . Map.elems
|
postProcess = Set.fromList . map fst . filter snd . Map.elems
|
||||||
|
|
||||||
|
recipientsListMsg <- messageI Info MsgCommRecipientsList
|
||||||
|
|
||||||
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication
|
((commRes,commWdgt),commEncoding) <- runFormPost . identifyForm FIDCommunication . renderAForm FormStandard $ Communication
|
||||||
<$> recipientAForm
|
<$> recipientAForm
|
||||||
|
<* aformMessage recipientsListMsg
|
||||||
<*> aopt textField (fslI MsgCommSubject) Nothing
|
<*> aopt textField (fslI MsgCommSubject) Nothing
|
||||||
<*> areq htmlField (fslpI MsgCommBody "Html") Nothing
|
<*> areq htmlField (fslpI MsgCommBody "Html" & setTooltip MsgCommBodyTip) Nothing
|
||||||
formResult commRes $ \comm -> do
|
formResult commRes $ \comm -> do
|
||||||
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
runDBJobs . runConduit $ transPipe (mapReaderT lift) (crJobs comm) .| sinkDBJobs
|
||||||
addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm
|
addMessageI Success . MsgCommSuccess . Set.size $ cRecipients comm
|
||||||
@ -183,4 +186,3 @@ commR CommunicationRoute{..} = do
|
|||||||
siteLayoutMsg crHeading $ do
|
siteLayoutMsg crHeading $ do
|
||||||
setTitleI crHeading
|
setTitleI crHeading
|
||||||
formWdgt
|
formWdgt
|
||||||
$(i18nWidgetFile "html-input")
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Handler.Utils.Csv
|
module Handler.Utils.Csv
|
||||||
( typeCsv, extensionCsv
|
( decodeCsv
|
||||||
, decodeCsv
|
|
||||||
, encodeCsv
|
, encodeCsv
|
||||||
, encodeDefaultOrderedCsv
|
, encodeDefaultOrderedCsv
|
||||||
, respondCsv, respondCsvDB
|
, respondCsv, respondCsvDB
|
||||||
@ -12,9 +11,6 @@ module Handler.Utils.Csv
|
|||||||
, ToNamedRecord(..), FromNamedRecord(..)
|
, ToNamedRecord(..), FromNamedRecord(..)
|
||||||
, DefaultOrdered(..)
|
, DefaultOrdered(..)
|
||||||
, ToField(..), FromField(..)
|
, ToField(..), FromField(..)
|
||||||
, CsvRendered(..)
|
|
||||||
, toCsvRendered
|
|
||||||
, toDefaultOrderedCsvRendered
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import hiding (Header, mapM_)
|
import Import hiding (Header, mapM_)
|
||||||
@ -40,18 +36,6 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
import qualified Data.Attoparsec.ByteString.Lazy as A
|
import qualified Data.Attoparsec.ByteString.Lazy as A
|
||||||
|
|
||||||
|
|
||||||
deriving instance Typeable CsvParseError
|
|
||||||
instance Exception CsvParseError
|
|
||||||
|
|
||||||
|
|
||||||
typeCsv, typeCsv' :: ContentType
|
|
||||||
typeCsv = simpleContentType typeCsv'
|
|
||||||
typeCsv' = "text/csv; charset=UTF-8; header=present"
|
|
||||||
|
|
||||||
extensionCsv :: Extension
|
|
||||||
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
|
|
||||||
|
|
||||||
|
|
||||||
decodeCsv :: (MonadThrow m, FromNamedRecord csv, MonadLogger m) => ConduitT ByteString csv m ()
|
decodeCsv :: (MonadThrow m, FromNamedRecord csv, MonadLogger m) => ConduitT ByteString csv m ()
|
||||||
decodeCsv = transPipe throwExceptT $ do
|
decodeCsv = transPipe throwExceptT $ do
|
||||||
testBuffer <- accumTestBuffer LBS.empty
|
testBuffer <- accumTestBuffer LBS.empty
|
||||||
@ -114,19 +98,23 @@ decodeCsv = transPipe throwExceptT $ do
|
|||||||
|
|
||||||
|
|
||||||
encodeCsv :: ( ToNamedRecord csv
|
encodeCsv :: ( ToNamedRecord csv
|
||||||
, Monad m
|
, MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
)
|
)
|
||||||
=> Header
|
=> Header
|
||||||
-> ConduitT csv ByteString m ()
|
-> ConduitT csv ByteString m ()
|
||||||
-- ^ Encode a stream of records
|
-- ^ Encode a stream of records
|
||||||
--
|
--
|
||||||
-- Currently not streaming
|
-- Currently not streaming
|
||||||
encodeCsv hdr = fmap (encodeByName hdr) (C.foldMap pure) >>= C.sourceLazy
|
encodeCsv hdr = do
|
||||||
|
csvOpts <- fmap (maybe def (userCsvOptions . entityVal)) . lift $ liftHandler maybeAuth
|
||||||
|
fmap (encodeByNameWith (csvOpts ^. _CsvEncodeOptions) hdr) (C.foldMap pure) >>= C.sourceLazy
|
||||||
|
|
||||||
encodeDefaultOrderedCsv :: forall csv m.
|
encodeDefaultOrderedCsv :: forall csv m.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
, Monad m
|
, MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
)
|
)
|
||||||
=> ConduitT csv ByteString m ()
|
=> ConduitT csv ByteString m ()
|
||||||
encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
|
encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
|
||||||
@ -134,33 +122,30 @@ encodeDefaultOrderedCsv = encodeCsv $ headerOrder (error "headerOrder" :: csv)
|
|||||||
|
|
||||||
respondCsv :: ToNamedRecord csv
|
respondCsv :: ToNamedRecord csv
|
||||||
=> Header
|
=> Header
|
||||||
-> ConduitT () csv (HandlerFor site) ()
|
-> ConduitT () csv Handler ()
|
||||||
-> HandlerFor site TypedContent
|
-> Handler TypedContent
|
||||||
respondCsv hdr src = respondSource typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
respondCsv hdr src = respondSource typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
||||||
|
|
||||||
respondDefaultOrderedCsv :: forall csv site.
|
respondDefaultOrderedCsv :: forall csv.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
)
|
)
|
||||||
=> ConduitT () csv (HandlerFor site) ()
|
=> ConduitT () csv Handler ()
|
||||||
-> HandlerFor site TypedContent
|
-> Handler TypedContent
|
||||||
respondDefaultOrderedCsv = respondCsv $ headerOrder (error "headerOrder" :: csv)
|
respondDefaultOrderedCsv = respondCsv $ headerOrder (error "headerOrder" :: csv)
|
||||||
|
|
||||||
respondCsvDB :: ( ToNamedRecord csv
|
respondCsvDB :: ToNamedRecord csv
|
||||||
, YesodPersistRunner site
|
|
||||||
)
|
|
||||||
=> Header
|
=> Header
|
||||||
-> ConduitT () csv (YesodDB site) ()
|
-> ConduitT () csv DB ()
|
||||||
-> HandlerFor site TypedContent
|
-> Handler TypedContent
|
||||||
respondCsvDB hdr src = respondSourceDB typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
respondCsvDB hdr src = respondSourceDB typeCsv' $ src .| encodeCsv hdr .| awaitForever sendChunk
|
||||||
|
|
||||||
respondDefaultOrderedCsvDB :: forall csv site.
|
respondDefaultOrderedCsvDB :: forall csv.
|
||||||
( ToNamedRecord csv
|
( ToNamedRecord csv
|
||||||
, DefaultOrdered csv
|
, DefaultOrdered csv
|
||||||
, YesodPersistRunner site
|
|
||||||
)
|
)
|
||||||
=> ConduitT () csv (YesodDB site) ()
|
=> ConduitT () csv DB ()
|
||||||
-> HandlerFor site TypedContent
|
-> Handler TypedContent
|
||||||
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
|
respondDefaultOrderedCsvDB = respondCsvDB $ headerOrder (error "headerOrder" :: csv)
|
||||||
|
|
||||||
fileSourceCsv :: ( FromNamedRecord csv
|
fileSourceCsv :: ( FromNamedRecord csv
|
||||||
@ -173,11 +158,6 @@ fileSourceCsv :: ( FromNamedRecord csv
|
|||||||
fileSourceCsv = (.| decodeCsv) . fileSource
|
fileSourceCsv = (.| decodeCsv) . fileSource
|
||||||
|
|
||||||
|
|
||||||
data CsvRendered = CsvRendered
|
|
||||||
{ csvRenderedHeader :: Header
|
|
||||||
, csvRenderedData :: [NamedRecord]
|
|
||||||
} deriving (Eq, Read, Show, Generic, Typeable)
|
|
||||||
|
|
||||||
instance ToWidget UniWorX CsvRendered where
|
instance ToWidget UniWorX CsvRendered where
|
||||||
toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
|
toWidget CsvRendered{..} = liftWidget $(widgetFile "widgets/csvRendered")
|
||||||
where
|
where
|
||||||
@ -188,21 +168,3 @@ instance ToWidget UniWorX CsvRendered where
|
|||||||
]
|
]
|
||||||
|
|
||||||
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
|
headers = decodeUtf8 <$> Vector.toList csvRenderedHeader
|
||||||
|
|
||||||
toCsvRendered :: forall mono.
|
|
||||||
( ToNamedRecord (Element mono)
|
|
||||||
, MonoFoldable mono
|
|
||||||
)
|
|
||||||
=> Header
|
|
||||||
-> mono -> CsvRendered
|
|
||||||
toCsvRendered csvRenderedHeader (otoList -> csvs) = CsvRendered{..}
|
|
||||||
where
|
|
||||||
csvRenderedData = map toNamedRecord csvs
|
|
||||||
|
|
||||||
toDefaultOrderedCsvRendered :: forall mono.
|
|
||||||
( ToNamedRecord (Element mono)
|
|
||||||
, DefaultOrdered (Element mono)
|
|
||||||
, MonoFoldable mono
|
|
||||||
)
|
|
||||||
=> mono -> CsvRendered
|
|
||||||
toDefaultOrderedCsvRendered = toCsvRendered $ headerOrder (error "headerOrder" :: Element mono)
|
|
||||||
|
|||||||
@ -78,12 +78,12 @@ examBonus (Entity eId Exam{..}) = runConduit $
|
|||||||
)
|
)
|
||||||
return (examRegistration E.^. ExamRegistrationUser, sheet E.^. SheetType, submission)
|
return (examRegistration E.^. ExamRegistrationUser, sheet E.^. SheetType, submission)
|
||||||
accum = C.fold ?? Map.empty $ \acc (E.Value uid, E.Value sheetType, fmap entityVal -> sub) ->
|
accum = C.fold ?? Map.empty $ \acc (E.Value uid, E.Value sheetType, fmap entityVal -> sub) ->
|
||||||
Map.unionWith mappend acc . Map.singleton uid . sheetTypeSum sheetType . (>>= submissionRatingPoints) $ assertM submissionRatingDone sub
|
flip (Map.insertWith mappend uid) acc . sheetTypeSum sheetType $ assertM submissionRatingDone sub >>= submissionRatingPoints
|
||||||
in rawData .| accum
|
in rawData .| accum
|
||||||
|
|
||||||
examBonusPossible, examBonusAchieved :: UserId -> Map UserId SheetTypeSummary -> Maybe SheetGradeSummary
|
examBonusPossible, examBonusAchieved :: UserId -> Map UserId SheetTypeSummary -> SheetGradeSummary
|
||||||
examBonusPossible uid bonusMap = normalSummary <$> Map.lookup uid bonusMap
|
examBonusPossible uid bonusMap = normalSummary $ Map.findWithDefault mempty uid bonusMap
|
||||||
examBonusAchieved uid bonusMap = (mappend <$> normalSummary <*> bonusSummary) <$> Map.lookup uid bonusMap
|
examBonusAchieved uid bonusMap = mappend <$> normalSummary <*> bonusSummary $ Map.findWithDefault mempty uid bonusMap
|
||||||
|
|
||||||
|
|
||||||
examResultBonus :: ExamBonusRule
|
examResultBonus :: ExamBonusRule
|
||||||
@ -91,6 +91,8 @@ examResultBonus :: ExamBonusRule
|
|||||||
-> SheetGradeSummary -- ^ `examBonusAchieved`
|
-> SheetGradeSummary -- ^ `examBonusAchieved`
|
||||||
-> Points
|
-> Points
|
||||||
examResultBonus bonusRule bonusPossible bonusAchieved = case bonusRule of
|
examResultBonus bonusRule bonusPossible bonusAchieved = case bonusRule of
|
||||||
|
ExamBonusManual{}
|
||||||
|
-> 0
|
||||||
ExamBonusPoints{..}
|
ExamBonusPoints{..}
|
||||||
-> roundToPoints bonusRound $ toRational bonusMaxPoints * bonusProp
|
-> roundToPoints bonusRound $ toRational bonusMaxPoints * bonusProp
|
||||||
where
|
where
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import Handler.Utils.Form.Types
|
|||||||
import Handler.Utils.DateTime
|
import Handler.Utils.DateTime
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
import Data.Char (chr, ord)
|
||||||
import qualified Data.Char as Char
|
import qualified Data.Char as Char
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
@ -220,11 +221,7 @@ multiAction :: forall action a.
|
|||||||
-> Maybe action
|
-> Maybe action
|
||||||
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
|
-> (Html -> MForm Handler (FormResult a, [FieldView UniWorX]))
|
||||||
multiAction acts fs@FieldSettings{..} defAction csrf = do
|
multiAction acts fs@FieldSettings{..} defAction csrf = do
|
||||||
mr <- getMessageRender
|
(actionRes, actionView) <- mreq (selectField . optionsF $ Map.keysSet acts) fs defAction
|
||||||
|
|
||||||
let
|
|
||||||
options = OptionList [ Option (mr a) a (toPathPiece a) | a <- Map.keys acts ] fromPathPiece
|
|
||||||
(actionRes, actionView) <- mreq (selectField $ return options) fs defAction
|
|
||||||
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
|
results <- mapM (fmap (over _2 ($ [])) . aFormToForm) acts
|
||||||
|
|
||||||
let actionResults = view _1 <$> results
|
let actionResults = view _1 <$> results
|
||||||
@ -520,7 +517,8 @@ submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ c
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
data ExamBonusRule' = ExamBonusPoints'
|
data ExamBonusRule' = ExamBonusManual'
|
||||||
|
| ExamBonusPoints'
|
||||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
instance Universe ExamBonusRule'
|
instance Universe ExamBonusRule'
|
||||||
instance Finite ExamBonusRule'
|
instance Finite ExamBonusRule'
|
||||||
@ -530,6 +528,7 @@ embedRenderMessage ''UniWorX ''ExamBonusRule' id
|
|||||||
|
|
||||||
classifyBonusRule :: ExamBonusRule -> ExamBonusRule'
|
classifyBonusRule :: ExamBonusRule -> ExamBonusRule'
|
||||||
classifyBonusRule = \case
|
classifyBonusRule = \case
|
||||||
|
ExamBonusManual{} -> ExamBonusManual'
|
||||||
ExamBonusPoints{} -> ExamBonusPoints'
|
ExamBonusPoints{} -> ExamBonusPoints'
|
||||||
|
|
||||||
examBonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule
|
examBonusRuleForm :: Maybe ExamBonusRule -> AForm Handler ExamBonusRule
|
||||||
@ -537,7 +536,11 @@ examBonusRuleForm prev = multiActionA actions (fslI MsgExamBonusRule) $ classify
|
|||||||
where
|
where
|
||||||
actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule)
|
actions :: Map ExamBonusRule' (AForm Handler ExamBonusRule)
|
||||||
actions = Map.fromList
|
actions = Map.fromList
|
||||||
[ ( ExamBonusPoints'
|
[ ( ExamBonusManual'
|
||||||
|
, ExamBonusManual
|
||||||
|
<$> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
|
||||||
|
)
|
||||||
|
, ( ExamBonusPoints'
|
||||||
, ExamBonusPoints
|
, ExamBonusPoints
|
||||||
<$> apreq (checkBool (> 0) MsgExamBonusMaxPointsNonPositive pointsField) (fslI MsgExamBonusMaxPoints & setTooltip MsgExamBonusMaxPointsTip) (preview _bonusMaxPoints =<< prev)
|
<$> apreq (checkBool (> 0) MsgExamBonusMaxPointsNonPositive pointsField) (fslI MsgExamBonusMaxPoints & setTooltip MsgExamBonusMaxPointsTip) (preview _bonusMaxPoints =<< prev)
|
||||||
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
|
<*> (fromMaybe False <$> aopt checkBoxField (fslI MsgExamBonusOnlyPassed) (Just <$> preview _bonusOnlyPassed =<< prev))
|
||||||
@ -1193,3 +1196,84 @@ examPassedField :: forall m.
|
|||||||
)
|
)
|
||||||
=> Field m ExamPassed
|
=> Field m ExamPassed
|
||||||
examPassedField = hoistField liftHandler $ selectField optionsFinite
|
examPassedField = hoistField liftHandler $ selectField optionsFinite
|
||||||
|
|
||||||
|
|
||||||
|
data CsvOptions' = CsvOptionsPreset' CsvPreset
|
||||||
|
| CsvOptionsCustom'
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriveFinite ''CsvOptions'
|
||||||
|
instance PathPiece CsvOptions' where
|
||||||
|
toPathPiece = \case
|
||||||
|
CsvOptionsPreset' p -> toPathPiece p
|
||||||
|
CsvOptionsCustom' -> "custom"
|
||||||
|
fromPathPiece t = fromPathPiece t
|
||||||
|
<|> guardOn (t == "custom") CsvOptionsCustom'
|
||||||
|
instance RenderMessage UniWorX CsvOptions' where
|
||||||
|
renderMessage m ls = \case
|
||||||
|
CsvOptionsPreset' p -> mr p
|
||||||
|
CsvOptionsCustom' -> mr MsgCsvCustom
|
||||||
|
where
|
||||||
|
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
|
||||||
|
mr = renderMessage m ls
|
||||||
|
|
||||||
|
csvOptionsForm :: forall m.
|
||||||
|
( MonadHandler m
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> FieldSettings UniWorX
|
||||||
|
-> Maybe CsvOptions
|
||||||
|
-> AForm m CsvOptions
|
||||||
|
csvOptionsForm fs mPrev = hoistAForm liftHandler . multiActionA csvActs fs $ classifyCsvOptions <$> mPrev
|
||||||
|
where
|
||||||
|
csvActs :: Map CsvOptions' (AForm Handler CsvOptions)
|
||||||
|
csvActs = mapF $ \case
|
||||||
|
CsvOptionsPreset' preset
|
||||||
|
-> pure $ csvPreset # preset
|
||||||
|
CsvOptionsCustom'
|
||||||
|
-> CsvOptions
|
||||||
|
<$> areq (selectField delimiterOpts) (fslI MsgCsvDelimiter) (csvDelimiter <$> mPrev)
|
||||||
|
<*> areq (selectField lineEndOpts) (fslI MsgCsvUseCrLf) (csvUseCrLf <$> mPrev)
|
||||||
|
<*> areq (selectField quoteOpts) (fslI MsgCsvQuoting & setTooltip MsgCsvQuotingTip) (csvQuoting <$> mPrev)
|
||||||
|
|
||||||
|
delimiterOpts :: Handler (OptionList Char)
|
||||||
|
delimiterOpts = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
let
|
||||||
|
opts =
|
||||||
|
[ (MsgCsvDelimiterNull, '\0')
|
||||||
|
, (MsgCsvDelimiterTab, '\t')
|
||||||
|
, (MsgCsvDelimiterComma, ',')
|
||||||
|
, (MsgCsvDelimiterColon, chr 58)
|
||||||
|
, (MsgCsvDelimiterBar, '|')
|
||||||
|
, (MsgCsvDelimiterSpace, ' ')
|
||||||
|
, (MsgCsvDelimiterUnitSep, chr 31)
|
||||||
|
]
|
||||||
|
olReadExternal t = do
|
||||||
|
i <- readMay t
|
||||||
|
guard $ i >= 0 && i <= 255
|
||||||
|
let c = chr i
|
||||||
|
guard $ any ((== c) . view _2) opts
|
||||||
|
return c
|
||||||
|
olOptions = [ Option (mr msg) c (tshow $ ord c)
|
||||||
|
| (msg, c) <- opts
|
||||||
|
]
|
||||||
|
return OptionList{..}
|
||||||
|
|
||||||
|
lineEndOpts :: Handler (OptionList Bool)
|
||||||
|
lineEndOpts = optionsPathPiece
|
||||||
|
[ (MsgCsvCrLf, True )
|
||||||
|
, (MsgCsvLf, False)
|
||||||
|
]
|
||||||
|
|
||||||
|
quoteOpts :: Handler (OptionList Quoting)
|
||||||
|
quoteOpts = optionsF
|
||||||
|
[ QuoteMinimal
|
||||||
|
, QuoteAll
|
||||||
|
]
|
||||||
|
|
||||||
|
classifyCsvOptions :: CsvOptions -> CsvOptions'
|
||||||
|
classifyCsvOptions opts
|
||||||
|
| Just preset <- opts ^? csvPreset
|
||||||
|
= CsvOptionsPreset' preset
|
||||||
|
| otherwise
|
||||||
|
= CsvOptionsCustom'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
module Handler.Utils.Mail
|
module Handler.Utils.Mail
|
||||||
( addRecipientsDB
|
( addRecipientsDB
|
||||||
, userAddress
|
, userAddress, userAddressFrom
|
||||||
, userMailT
|
, userMailT
|
||||||
, addFileDB
|
, addFileDB
|
||||||
) where
|
) where
|
||||||
@ -28,7 +28,16 @@ addRecipientsDB uFilter = runConduit $ transPipe (liftHandler . runDB) (selectSo
|
|||||||
let addr = Address (Just userDisplayName) $ CI.original userEmail
|
let addr = Address (Just userDisplayName) $ CI.original userEmail
|
||||||
_mailTo %= flip snoc addr
|
_mailTo %= flip snoc addr
|
||||||
|
|
||||||
|
userAddressFrom :: User -> Address
|
||||||
|
-- ^ Format an e-mail address suitable for usage in a @From@-header
|
||||||
|
--
|
||||||
|
-- Uses `userDisplayEmail`
|
||||||
|
userAddressFrom User{userDisplayEmail, userDisplayName} = Address (Just userDisplayName) $ CI.original userDisplayEmail
|
||||||
|
|
||||||
userAddress :: User -> Address
|
userAddress :: User -> Address
|
||||||
|
-- ^ Format an e-mail address suitable for usage as a recipient
|
||||||
|
--
|
||||||
|
-- Uses `userEmail`
|
||||||
userAddress User{userEmail, userDisplayName} = Address (Just userDisplayName) $ CI.original userEmail
|
userAddress User{userEmail, userDisplayName} = Address (Just userDisplayName) $ CI.original userEmail
|
||||||
|
|
||||||
userMailT :: ( MonadHandler m
|
userMailT :: ( MonadHandler m
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import Control.Monad.State.Class as State
|
|||||||
import Control.Monad.Writer (MonadWriter(..), execWriterT, execWriter)
|
import Control.Monad.Writer (MonadWriter(..), execWriterT, execWriter)
|
||||||
import Control.Monad.RWS.Lazy (MonadRWS, RWST, execRWST)
|
import Control.Monad.RWS.Lazy (MonadRWS, RWST, execRWST)
|
||||||
import qualified Control.Monad.Random as Rand
|
import qualified Control.Monad.Random as Rand
|
||||||
import qualified System.Random.Shuffle as Rand (shuffleM)
|
|
||||||
|
|
||||||
import Data.Maybe ()
|
import Data.Maybe ()
|
||||||
|
|
||||||
@ -248,9 +247,6 @@ planSubmissions sid restriction = do
|
|||||||
maximumsBy :: (Ord a, Ord b) => (a -> b) -> Set a -> Set a
|
maximumsBy :: (Ord a, Ord b) => (a -> b) -> Set a -> Set a
|
||||||
maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs
|
maximumsBy f xs = flip Set.filter xs $ \x -> maybe True (((==) `on` f) x . maximumBy (comparing f)) $ fromNullable xs
|
||||||
|
|
||||||
unstableSortBy :: MonadRandom m => (a -> a -> Ordering) -> [a] -> m [a]
|
|
||||||
unstableSortBy cmp = fmap concat . mapM Rand.shuffleM . groupBy (\a b -> cmp a b == EQ) . sortBy cmp
|
|
||||||
|
|
||||||
|
|
||||||
submissionFileSource :: SubmissionId -> ConduitT () (Entity File) (YesodDB UniWorX) ()
|
submissionFileSource :: SubmissionId -> ConduitT () (Entity File) (YesodDB UniWorX) ()
|
||||||
submissionFileSource = E.selectSource . fmap snd . E.from . submissionFileQuery
|
submissionFileSource = E.selectSource . fmap snd . E.from . submissionFileQuery
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import Handler.Utils.Form
|
|||||||
import Handler.Utils.Csv
|
import Handler.Utils.Csv
|
||||||
import Handler.Utils.ContentDisposition
|
import Handler.Utils.ContentDisposition
|
||||||
import Handler.Utils.I18n
|
import Handler.Utils.I18n
|
||||||
|
import Handler.Utils.Widgets
|
||||||
import Utils
|
import Utils
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
|
|
||||||
|
|||||||
@ -96,3 +96,9 @@ editedByW fmt tm usr = do
|
|||||||
heat :: Integral a => a -> a -> Double
|
heat :: Integral a => a -> a -> Double
|
||||||
heat (toInteger -> full) (toInteger -> achieved)
|
heat (toInteger -> full) (toInteger -> achieved)
|
||||||
= roundToDigits 3 $ cutOffPercent 0.3 (fromIntegral full^2) (fromIntegral achieved^2)
|
= roundToDigits 3 $ cutOffPercent 0.3 (fromIntegral full^2) (fromIntegral achieved^2)
|
||||||
|
|
||||||
|
i18n :: forall m msg.
|
||||||
|
( MonadWidget m
|
||||||
|
, RenderMessage (HandlerSite m) msg
|
||||||
|
) => msg -> m ()
|
||||||
|
i18n = toWidget . (SomeMessage :: msg -> SomeMessage (HandlerSite m))
|
||||||
|
|||||||
@ -84,6 +84,14 @@ import Control.Monad.Trans.Reader as Import
|
|||||||
( reader, Reader, runReader, mapReader, withReader
|
( reader, Reader, runReader, mapReader, withReader
|
||||||
, ReaderT(..), mapReaderT, withReaderT
|
, ReaderT(..), mapReaderT, withReaderT
|
||||||
)
|
)
|
||||||
|
import Control.Monad.Trans.State as Import
|
||||||
|
( state, State, runState, mapState, withState
|
||||||
|
, StateT(..), mapStateT, withStateT
|
||||||
|
)
|
||||||
|
import Control.Monad.Trans.Writer.Lazy as Import
|
||||||
|
( writer, Writer, runWriter, mapWriter, execWriter
|
||||||
|
, WriterT(..), mapWriterT, execWriterT
|
||||||
|
)
|
||||||
import Control.Monad.Base as Import
|
import Control.Monad.Base as Import
|
||||||
import Control.Monad.Catch as Import hiding (Handler(..))
|
import Control.Monad.Catch as Import hiding (Handler(..))
|
||||||
import Control.Monad.Trans.Control as Import hiding (embed)
|
import Control.Monad.Trans.Control as Import hiding (embed)
|
||||||
|
|||||||
41
src/Jobs.hs
41
src/Jobs.hs
@ -86,6 +86,7 @@ instance Exception JobQueueException
|
|||||||
handleJobs :: ( MonadResource m
|
handleJobs :: ( MonadResource m
|
||||||
, MonadLogger m
|
, MonadLogger m
|
||||||
, MonadUnliftIO m
|
, MonadUnliftIO m
|
||||||
|
, MonadMask m
|
||||||
)
|
)
|
||||||
=> UniWorX -> m ()
|
=> UniWorX -> m ()
|
||||||
-- | Spawn a set of workers that read control commands from `appJobCtl` and address them as they come in
|
-- | Spawn a set of workers that read control commands from `appJobCtl` and address them as they come in
|
||||||
@ -97,7 +98,7 @@ handleJobs foundation@UniWorX{..}
|
|||||||
| otherwise = do
|
| otherwise = do
|
||||||
UnliftIO{..} <- askUnliftIO
|
UnliftIO{..} <- askUnliftIO
|
||||||
|
|
||||||
jobPoolManager <- allocateLinkedAsync . unliftIO $ manageJobPool foundation
|
jobPoolManager <- allocateLinkedAsyncWithUnmask $ \unmask -> unliftIO $ manageJobPool foundation unmask
|
||||||
|
|
||||||
jobCron <- allocateLinkedAsync . unliftIO $ manageCrontab foundation
|
jobCron <- allocateLinkedAsync . unliftIO $ manageCrontab foundation
|
||||||
|
|
||||||
@ -129,20 +130,40 @@ manageJobPool :: forall m.
|
|||||||
( MonadResource m
|
( MonadResource m
|
||||||
, MonadLogger m
|
, MonadLogger m
|
||||||
, MonadUnliftIO m
|
, MonadUnliftIO m
|
||||||
|
, MonadMask m
|
||||||
)
|
)
|
||||||
=> UniWorX -> m ()
|
=> UniWorX -> (forall a. IO a -> IO a) -> m ()
|
||||||
manageJobPool foundation@UniWorX{..}
|
manageJobPool foundation@UniWorX{..} unmask = shutdownOnException $
|
||||||
= flip runContT return . forever . join . atomically $ asum
|
flip runContT return . forever . join . atomically $ asum
|
||||||
[ spawnMissingWorkers
|
[ spawnMissingWorkers
|
||||||
, reapDeadWorkers
|
, reapDeadWorkers
|
||||||
, terminateGracefully
|
, terminateGracefully
|
||||||
]
|
]
|
||||||
where
|
where
|
||||||
|
shutdownOnException :: m a -> m a
|
||||||
|
shutdownOnException act = do
|
||||||
|
UnliftIO{..} <- askUnliftIO
|
||||||
|
|
||||||
|
actAsync <- allocateLinkedAsyncMasked $ unliftIO act
|
||||||
|
|
||||||
|
let handleExc e = do
|
||||||
|
atomically $ do
|
||||||
|
jState <- tryReadTMVar appJobState
|
||||||
|
for_ jState $ \JobState{jobShutdown} -> tryPutTMVar jobShutdown ()
|
||||||
|
|
||||||
|
void $ wait actAsync
|
||||||
|
throwM e
|
||||||
|
|
||||||
|
liftIO (unmask $ wait actAsync) `catchAll` handleExc
|
||||||
|
|
||||||
num :: Int
|
num :: Int
|
||||||
num = fromIntegral $ foundation ^. _appJobWorkers
|
num = fromIntegral $ foundation ^. _appJobWorkers
|
||||||
|
|
||||||
spawnMissingWorkers, reapDeadWorkers, terminateGracefully :: STM (ContT () m ())
|
spawnMissingWorkers, reapDeadWorkers, terminateGracefully :: STM (ContT () m ())
|
||||||
spawnMissingWorkers = do
|
spawnMissingWorkers = do
|
||||||
|
shouldTerminate' <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
||||||
|
guard $ not shouldTerminate'
|
||||||
|
|
||||||
oldState <- takeTMVar appJobState
|
oldState <- takeTMVar appJobState
|
||||||
let missing = num - Map.size (jobWorkers oldState)
|
let missing = num - Map.size (jobWorkers oldState)
|
||||||
guard $ missing > 0
|
guard $ missing > 0
|
||||||
@ -204,6 +225,10 @@ manageJobPool foundation@UniWorX{..}
|
|||||||
terminateGracefully = do
|
terminateGracefully = do
|
||||||
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
shouldTerminate <- readTMVar appJobState >>= fmap not . isEmptyTMVar . jobShutdown
|
||||||
guard shouldTerminate
|
guard shouldTerminate
|
||||||
|
|
||||||
|
oldState <- takeTMVar appJobState
|
||||||
|
guard $ 0 == Map.size (jobWorkers oldState)
|
||||||
|
|
||||||
return . callCC $ \terminate -> do
|
return . callCC $ \terminate -> do
|
||||||
$logInfoS "JobPoolManager" "Shutting down"
|
$logInfoS "JobPoolManager" "Shutting down"
|
||||||
terminate ()
|
terminate ()
|
||||||
|
|||||||
@ -20,7 +20,7 @@ dispatchJobInvitation jInviter jInvitee jInvitationUrl jInvitationSubject jInvit
|
|||||||
|
|
||||||
whenIsJust mInviter $ \jInviter' -> mailT def $ do
|
whenIsJust mInviter $ \jInviter' -> mailT def $ do
|
||||||
_mailTo .= [Address Nothing $ CI.original jInvitee]
|
_mailTo .= [Address Nothing $ CI.original jInvitee]
|
||||||
replaceMailHeader "Reply-To" . Just . renderAddress $ userAddress jInviter'
|
replaceMailHeader "Reply-To" . Just . renderAddress $ userAddressFrom jInviter'
|
||||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
replaceMailHeader "Subject" $ Just jInvitationSubject
|
replaceMailHeader "Subject" $ Just jInvitationSubject
|
||||||
addPart ($(ihamletFile "templates/mail/invitation.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
addPart ($(ihamletFile "templates/mail/invitation.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import Import
|
|||||||
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
@ -24,13 +22,15 @@ dispatchJobSendCourseCommunication jRecipientEmail jAllRecipientAddresses jCours
|
|||||||
<$> getJust jSender
|
<$> getJust jSender
|
||||||
<*> getJust jCourse
|
<*> getJust jCourse
|
||||||
either (\email -> mailT def . (assign _mailTo (pure . Address Nothing $ CI.original email) *>)) userMailT jRecipientEmail $ do
|
either (\email -> mailT def . (assign _mailTo (pure . Address Nothing $ CI.original email) *>)) userMailT jRecipientEmail $ do
|
||||||
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
|
|
||||||
void $ setMailObjectUUID jMailObjectUUID
|
void $ setMailObjectUUID jMailObjectUUID
|
||||||
_mailFrom .= userAddress sender
|
_mailFrom .= userAddressFrom sender
|
||||||
if -- Use `addMailHeader` instead of `_mailCc` to make `mailT` ignore the additional recipients
|
addMailHeader "Cc" [st|#{mr MsgCommUndisclosedRecipients}:;|]
|
||||||
| jRecipientEmail == Right jSender
|
|
||||||
-> addMailHeader "Cc" . intercalate ", " . map renderAddress $ Set.toAscList (Set.delete (userAddress sender) jAllRecipientAddresses)
|
|
||||||
| otherwise
|
|
||||||
-> addMailHeader "Cc" "Undisclosed Recipients:;"
|
|
||||||
addMailHeader "Auto-Submitted" "no"
|
addMailHeader "Auto-Submitted" "no"
|
||||||
setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject
|
setSubjectI . prependCourseTitle courseTerm courseSchool courseShorthand $ maybe (SomeMessage MsgCommCourseSubject) SomeMessage jSubject
|
||||||
void $ addPart jMailContent
|
void $ addPart jMailContent
|
||||||
|
when (jRecipientEmail == Right jSender) $
|
||||||
|
addPart' $ do
|
||||||
|
partIsAttachment $ unpack (mr MsgCommAllRecipients) `addExtension` unpack extensionCsv
|
||||||
|
toMailPart (toDefaultOrderedCsvRendered jAllRecipientAddresses, userCsvOptions sender)
|
||||||
|
|||||||
@ -23,7 +23,7 @@ dispatchNotificationSubmissionRated nSubmission jRecipient = userMailT jRecipien
|
|||||||
return (course, sheet, submission, corrector)
|
return (course, sheet, submission, corrector)
|
||||||
|
|
||||||
whenIsJust corrector $ \corrector' ->
|
whenIsJust corrector $ \corrector' ->
|
||||||
addMailHeader "Reply-To" . renderAddress $ userAddress corrector'
|
addMailHeader "Reply-To" . renderAddress $ userAddressFrom corrector'
|
||||||
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
setSubjectI $ MsgMailSubjectSubmissionRated courseShorthand
|
||||||
|
|
||||||
|
|||||||
34
src/Mail.hs
34
src/Mail.hs
@ -21,7 +21,7 @@ module Mail
|
|||||||
, PrioritisedAlternatives
|
, PrioritisedAlternatives
|
||||||
, ToMailPart(..)
|
, ToMailPart(..)
|
||||||
, addAlternatives, provideAlternative, providePreferredAlternative
|
, addAlternatives, provideAlternative, providePreferredAlternative
|
||||||
, addPart
|
, addPart, addPart', modifyPart, partIsAttachment
|
||||||
, MonadHeader(..)
|
, MonadHeader(..)
|
||||||
, MailHeader
|
, MailHeader
|
||||||
, MailObjectId
|
, MailObjectId
|
||||||
@ -43,6 +43,8 @@ import Model.Types.TH.JSON
|
|||||||
import Network.Mail.Mime hiding (addPart, addAttachment)
|
import Network.Mail.Mime hiding (addPart, addAttachment)
|
||||||
import qualified Network.Mail.Mime as Mime (addPart)
|
import qualified Network.Mail.Mime as Mime (addPart)
|
||||||
|
|
||||||
|
import Settings.Mime
|
||||||
|
|
||||||
import Data.Monoid (Last(..))
|
import Data.Monoid (Last(..))
|
||||||
import Control.Monad.Trans.RWS (RWST(..))
|
import Control.Monad.Trans.RWS (RWST(..))
|
||||||
import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT)
|
import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT)
|
||||||
@ -71,6 +73,7 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
|
|
||||||
import Utils (MsgRendererS(..), MonadSecretBox(..), maybeT)
|
import Utils (MsgRendererS(..), MonadSecretBox(..), maybeT)
|
||||||
import Utils.Lens.TH
|
import Utils.Lens.TH
|
||||||
|
|
||||||
import Control.Lens hiding (from)
|
import Control.Lens hiding (from)
|
||||||
import Control.Lens.Extras (is)
|
import Control.Lens.Extras (is)
|
||||||
|
|
||||||
@ -336,7 +339,7 @@ instance YesodMail site => ToMailPart site (StateT Part (HandlerFor site) a) whe
|
|||||||
|
|
||||||
instance YesodMail site => ToMailPart site LT.Text where
|
instance YesodMail site => ToMailPart site LT.Text where
|
||||||
toMailPart text = do
|
toMailPart text = do
|
||||||
_partType .= "text/plain; charset=utf-8"
|
_partType .= decodeUtf8 typePlain
|
||||||
_partEncoding .= QuotedPrintableText
|
_partEncoding .= QuotedPrintableText
|
||||||
_partContent .= encodeUtf8 text
|
_partContent .= encodeUtf8 text
|
||||||
|
|
||||||
@ -348,7 +351,7 @@ instance YesodMail site => ToMailPart site LTB.Builder where
|
|||||||
|
|
||||||
instance YesodMail site => ToMailPart site Html where
|
instance YesodMail site => ToMailPart site Html where
|
||||||
toMailPart html = do
|
toMailPart html = do
|
||||||
_partType .= "text/html; charset=utf-8"
|
_partType .= decodeUtf8 typeHtml
|
||||||
_partEncoding .= QuotedPrintableText
|
_partEncoding .= QuotedPrintableText
|
||||||
_partContent .= renderMarkup html
|
_partContent .= renderMarkup html
|
||||||
|
|
||||||
@ -372,7 +375,7 @@ instance ToMailPart site a => ToMailPart site (Shakespeare.RenderUrl (Route site
|
|||||||
|
|
||||||
instance YesodMail site => ToMailPart site Aeson.Value where
|
instance YesodMail site => ToMailPart site Aeson.Value where
|
||||||
toMailPart val = do
|
toMailPart val = do
|
||||||
_partType .= "application/json; charset=utf-8"
|
_partType .= decodeUtf8 typeJson
|
||||||
_partEncoding .= QuotedPrintableText
|
_partEncoding .= QuotedPrintableText
|
||||||
_partContent .= Aeson.encodePretty val
|
_partContent .= Aeson.encodePretty val
|
||||||
|
|
||||||
@ -396,20 +399,35 @@ addPart :: ( MonadMail m
|
|||||||
, HandlerSite m ~ site
|
, HandlerSite m ~ site
|
||||||
, ToMailPart site a
|
, ToMailPart site a
|
||||||
) => a -> m (MailPartReturn site a)
|
) => a -> m (MailPartReturn site a)
|
||||||
addPart part = do
|
addPart = addPart' . toMailPart
|
||||||
(ret, part') <- runStateT (toMailPart part) initialPart
|
|
||||||
|
addPart' :: MonadMail m
|
||||||
|
=> StateT Part m a
|
||||||
|
-> m a
|
||||||
|
addPart' part = do
|
||||||
|
(ret, part') <- runStateT part initialPart
|
||||||
modify . Mime.addPart $ pure part'
|
modify . Mime.addPart $ pure part'
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
initialPart :: Part
|
initialPart :: Part
|
||||||
initialPart = Part
|
initialPart = Part
|
||||||
{ partType = "text/plain"
|
{ partType = decodeUtf8 defaultMimeType
|
||||||
, partEncoding = None
|
, partEncoding = Base64
|
||||||
, partFilename = Nothing
|
, partFilename = Nothing
|
||||||
, partHeaders = []
|
, partHeaders = []
|
||||||
, partContent = mempty
|
, partContent = mempty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modifyPart :: (MonadMail m, HandlerSite m ~ site, YesodMail site)
|
||||||
|
=> StateT Part (HandlerFor site) a
|
||||||
|
-> StateT Part m a
|
||||||
|
modifyPart = toMailPart
|
||||||
|
|
||||||
|
partIsAttachment :: (Textual t, MonadMail m, HandlerSite m ~ site, YesodMail site)
|
||||||
|
=> t
|
||||||
|
-> StateT Part m ()
|
||||||
|
partIsAttachment (repack -> fName) = modifyPart $ _partFilename .= Just fName
|
||||||
|
|
||||||
|
|
||||||
class MonadHandler m => MonadHeader m where
|
class MonadHandler m => MonadHeader m where
|
||||||
modifyHeaders :: (Headers -> Headers) -> m ()
|
modifyHeaders :: (Headers -> Headers) -> m ()
|
||||||
|
|||||||
@ -13,6 +13,7 @@ module Model.Types.Exam
|
|||||||
, ExamOccurrenceRule(..)
|
, ExamOccurrenceRule(..)
|
||||||
, ExamGrade(..)
|
, ExamGrade(..)
|
||||||
, numberGrade
|
, numberGrade
|
||||||
|
, ExamGradeDefCenter(..)
|
||||||
, ExamGradingRule(..)
|
, ExamGradingRule(..)
|
||||||
, ExamPassed(..)
|
, ExamPassed(..)
|
||||||
, passingGrade
|
, passingGrade
|
||||||
@ -116,7 +117,10 @@ instance Universe res => Universe (ExamResult' res) where
|
|||||||
instance Finite res => Finite (ExamResult' res)
|
instance Finite res => Finite (ExamResult' res)
|
||||||
|
|
||||||
|
|
||||||
data ExamBonusRule = ExamBonusPoints
|
data ExamBonusRule = ExamBonusManual
|
||||||
|
{ bonusOnlyPassed :: Bool
|
||||||
|
}
|
||||||
|
| ExamBonusPoints
|
||||||
{ bonusMaxPoints :: Points
|
{ bonusMaxPoints :: Points
|
||||||
, bonusOnlyPassed :: Bool
|
, bonusOnlyPassed :: Bool
|
||||||
, bonusRound :: Points
|
, bonusRound :: Points
|
||||||
@ -215,6 +219,15 @@ instance PersistFieldSql ExamGrade where
|
|||||||
sqlType _ = SqlNumeric 2 1
|
sqlType _ = SqlNumeric 2 1
|
||||||
|
|
||||||
|
|
||||||
|
newtype ExamGradeDefCenter = ExamGradeDefCenter { examGradeDefCenter :: Maybe ExamGrade }
|
||||||
|
deriving (Eq, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
instance Ord ExamGradeDefCenter where
|
||||||
|
ExamGradeDefCenter Nothing <= ExamGradeDefCenter (Just g) = Grade23 <= g
|
||||||
|
ExamGradeDefCenter (Just g) <= ExamGradeDefCenter Nothing = g <= Grade27
|
||||||
|
ExamGradeDefCenter g <= ExamGradeDefCenter g' = g <= g'
|
||||||
|
|
||||||
|
|
||||||
data ExamGradingRule
|
data ExamGradingRule
|
||||||
= ExamGradingKey
|
= ExamGradingKey
|
||||||
{ examGradingKey :: [Points] -- ^ @[n1, n2, n3, ..., n11]@ means @0 <= p < n1 -> p ~= 5@, @n1 <= p < n2 -> p ~ 4@, @n2 <= p < n3 -> p ~ 3.7@, ..., @n10 <= p -> p ~ 1.0@
|
{ examGradingKey :: [Points] -- ^ @[n1, n2, n3, ..., n11]@ means @0 <= p < n1 -> p ~= 5@, @n1 <= p < n2 -> p ~ 4@, @n2 <= p < n3 -> p ~ 3.7@, ..., @n10 <= p -> p ~ 1.0@
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Module: Model.Types.Misc
|
Module: Model.Types.Misc
|
||||||
Description: Additional uncategorized types
|
Description: Additional uncategorized types
|
||||||
@ -5,6 +7,7 @@ Description: Additional uncategorized types
|
|||||||
|
|
||||||
module Model.Types.Misc
|
module Model.Types.Misc
|
||||||
( module Model.Types.Misc
|
( module Model.Types.Misc
|
||||||
|
, Quoting(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
@ -14,6 +17,11 @@ import Data.Maybe (fromJust)
|
|||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Lens as Text
|
import qualified Data.Text.Lens as Text
|
||||||
|
|
||||||
|
import Data.Csv (Quoting(..))
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
|
||||||
data StudyFieldType = FieldPrimary | FieldSecondary
|
data StudyFieldType = FieldPrimary | FieldSecondary
|
||||||
deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic)
|
deriving (Eq, Ord, Enum, Show, Read, Bounded, Generic)
|
||||||
@ -43,3 +51,83 @@ nullaryPathPiece ''Theme $ camelToPathPiece' 1
|
|||||||
$(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate " " . unsafeTail . splitCamel) ''Theme) -- describe theme to user
|
$(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate " " . unsafeTail . splitCamel) ''Theme) -- describe theme to user
|
||||||
|
|
||||||
derivePersistField "Theme"
|
derivePersistField "Theme"
|
||||||
|
|
||||||
|
|
||||||
|
deriving instance Generic Quoting
|
||||||
|
deriving instance Ord Quoting
|
||||||
|
deriving instance Read Quoting
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = camelToPathPiece' 1
|
||||||
|
} ''Quoting
|
||||||
|
deriveFinite ''Quoting
|
||||||
|
nullaryPathPiece ''Quoting $ \q -> if
|
||||||
|
| q == "QuoteNone" -> "never"
|
||||||
|
| otherwise -> camelToPathPiece' 1 q
|
||||||
|
|
||||||
|
data CsvOptions
|
||||||
|
= CsvOptions
|
||||||
|
{ csvDelimiter :: Char
|
||||||
|
, csvUseCrLf :: Bool
|
||||||
|
, csvQuoting :: Csv.Quoting
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
instance Default CsvOptions where
|
||||||
|
def = csvPreset # CsvPresetRFC
|
||||||
|
|
||||||
|
data CsvPreset = CsvPresetRFC
|
||||||
|
| CsvPresetExcel
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
instance Universe CsvPreset
|
||||||
|
instance Finite CsvPreset
|
||||||
|
|
||||||
|
csvPreset :: Prism' CsvOptions CsvPreset
|
||||||
|
csvPreset = prism' fromPreset toPreset
|
||||||
|
where
|
||||||
|
fromPreset :: CsvPreset -> CsvOptions
|
||||||
|
fromPreset CsvPresetRFC = CsvOptions { csvDelimiter = ',', csvUseCrLf = True, csvQuoting = QuoteMinimal }
|
||||||
|
fromPreset CsvPresetExcel = CsvOptions { csvDelimiter = ';', csvUseCrLf = True, csvQuoting = QuoteAll }
|
||||||
|
|
||||||
|
toPreset :: CsvOptions -> Maybe CsvPreset
|
||||||
|
toPreset opts = case filter (\p -> fromPreset p == opts) universeF of
|
||||||
|
[p] -> Just p
|
||||||
|
_other -> Nothing
|
||||||
|
|
||||||
|
_CsvEncodeOptions :: Iso' CsvOptions Csv.EncodeOptions
|
||||||
|
_CsvEncodeOptions = iso toEncode fromEncode
|
||||||
|
where
|
||||||
|
toEncode CsvOptions{..} = Csv.defaultEncodeOptions
|
||||||
|
{ Csv.encDelimiter = fromIntegral $ fromEnum csvDelimiter
|
||||||
|
, Csv.encUseCrLf = csvUseCrLf
|
||||||
|
, Csv.encQuoting = csvQuoting
|
||||||
|
, Csv.encIncludeHeader = True
|
||||||
|
}
|
||||||
|
fromEncode encOpts = CsvOptions
|
||||||
|
{ csvDelimiter = toEnum . fromIntegral $ Csv.encDelimiter encOpts
|
||||||
|
, csvUseCrLf = Csv.encUseCrLf encOpts
|
||||||
|
, csvQuoting = Csv.encQuoting encOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
instance ToJSON CsvOptions where
|
||||||
|
toJSON CsvOptions{..} = JSON.object
|
||||||
|
[ "delimiter" JSON..= fromEnum csvDelimiter
|
||||||
|
, "use-cr-lf" JSON..= csvUseCrLf
|
||||||
|
, "quoting" JSON..= csvQuoting
|
||||||
|
]
|
||||||
|
instance FromJSON CsvOptions where
|
||||||
|
parseJSON = JSON.withObject "CsvOptions" $ \o -> do
|
||||||
|
csvDelimiter <- fmap (fmap toEnum) (o JSON..:? "delimiter") JSON..!= csvDelimiter def
|
||||||
|
csvUseCrLf <- o JSON..:? "use-cr-lf" JSON..!= csvUseCrLf def
|
||||||
|
csvQuoting <- o JSON..:? "quoting" JSON..!= csvQuoting def
|
||||||
|
return CsvOptions{..}
|
||||||
|
derivePersistFieldJSON ''CsvOptions
|
||||||
|
|
||||||
|
nullaryPathPiece ''CsvPreset $ camelToPathPiece' 2
|
||||||
|
|
||||||
|
instance YesodMail site => ToMailPart site (CsvRendered, CsvOptions) where
|
||||||
|
toMailPart (CsvRendered{..}, encOpts) = do
|
||||||
|
_partType .= decodeUtf8 typeCsv'
|
||||||
|
_partEncoding .= QuotedPrintableText
|
||||||
|
_partContent .= Csv.encodeByNameWith (encOpts ^. _CsvEncodeOptions) csvRenderedHeader csvRenderedData
|
||||||
|
instance YesodMail site => ToMailPart site CsvRendered where
|
||||||
|
toMailPart = toMailPart . (, def :: CsvOptions)
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import Utils.PathPiece
|
|||||||
|
|
||||||
import Utils (assertM)
|
import Utils (assertM)
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
|
||||||
deriving instance Read Address
|
deriving instance Read Address
|
||||||
deriving instance Ord Address
|
deriving instance Ord Address
|
||||||
@ -32,3 +34,13 @@ instance FromJSON Address where
|
|||||||
addressName <- assertM (not . null) <$> (obj .:? "name")
|
addressName <- assertM (not . null) <$> (obj .:? "name")
|
||||||
addressEmail <- obj .: "email"
|
addressEmail <- obj .: "email"
|
||||||
return Address{..}
|
return Address{..}
|
||||||
|
|
||||||
|
|
||||||
|
instance Csv.ToNamedRecord Address where
|
||||||
|
toNamedRecord Address{..} = Csv.namedRecord
|
||||||
|
[ "name" Csv..= addressName
|
||||||
|
, "email" Csv..= addressEmail
|
||||||
|
]
|
||||||
|
|
||||||
|
instance Csv.DefaultOrdered Address where
|
||||||
|
headerOrder _ = Csv.header [ "name", "email" ]
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
module Settings
|
module Settings
|
||||||
( module Settings
|
( module Settings
|
||||||
, module Settings.Cluster
|
, module Settings.Cluster
|
||||||
|
, module Settings.Mime
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import.NoModel
|
import Import.NoModel
|
||||||
@ -58,6 +59,7 @@ import qualified Database.Memcached.Binary.Types as Memcached
|
|||||||
|
|
||||||
import Model
|
import Model
|
||||||
import Settings.Cluster
|
import Settings.Cluster
|
||||||
|
import Settings.Mime
|
||||||
|
|
||||||
import Control.Monad.Trans.Maybe (MaybeT(..))
|
import Control.Monad.Trans.Maybe (MaybeT(..))
|
||||||
|
|
||||||
@ -67,10 +69,6 @@ import Jose.Jwt (JwtEncoding(..))
|
|||||||
|
|
||||||
import System.FilePath.Glob
|
import System.FilePath.Glob
|
||||||
import Handler.Utils.Submission.TH
|
import Handler.Utils.Submission.TH
|
||||||
import Network.Mime.TH
|
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
import qualified Data.Set as Set
|
|
||||||
|
|
||||||
|
|
||||||
-- | Runtime settings to configure this application. These settings can be
|
-- | Runtime settings to configure this application. These settings can be
|
||||||
@ -458,18 +456,6 @@ widgetFileSettings = def
|
|||||||
submissionBlacklist :: [Pattern]
|
submissionBlacklist :: [Pattern]
|
||||||
submissionBlacklist = $(patternFile compDefault "config/submission-blacklist")
|
submissionBlacklist = $(patternFile compDefault "config/submission-blacklist")
|
||||||
|
|
||||||
mimeMap :: MimeMap
|
|
||||||
mimeMap = $(mimeMapFile "config/mimetypes")
|
|
||||||
|
|
||||||
mimeLookup :: FileName -> MimeType
|
|
||||||
mimeLookup = mimeByExt mimeMap defaultMimeType
|
|
||||||
|
|
||||||
mimeExtensions :: MimeType -> Set Extension
|
|
||||||
mimeExtensions needle = Set.fromList [ ext | (ext, typ) <- Map.toList mimeMap, typ == needle ]
|
|
||||||
|
|
||||||
archiveTypes :: Set MimeType
|
|
||||||
archiveTypes = $(mimeSetFile "config/archive-types")
|
|
||||||
|
|
||||||
-- The rest of this file contains settings which rarely need changing by a
|
-- The rest of this file contains settings which rarely need changing by a
|
||||||
-- user.
|
-- user.
|
||||||
|
|
||||||
|
|||||||
31
src/Settings/Mime.hs
Normal file
31
src/Settings/Mime.hs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module Settings.Mime
|
||||||
|
( mimeMap
|
||||||
|
, mimeLookup
|
||||||
|
, mimeExtensions
|
||||||
|
, archiveTypes
|
||||||
|
, module Network.Mime
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Network.Mime
|
||||||
|
( FileName, MimeType, MimeMap, Extension
|
||||||
|
, mimeByExt, defaultMimeType
|
||||||
|
)
|
||||||
|
import Network.Mime.TH
|
||||||
|
|
||||||
|
|
||||||
|
mimeMap :: MimeMap
|
||||||
|
mimeMap = $(mimeMapFile "config/mimetypes")
|
||||||
|
|
||||||
|
mimeLookup :: FileName -> MimeType
|
||||||
|
mimeLookup = mimeByExt mimeMap defaultMimeType
|
||||||
|
|
||||||
|
mimeExtensions :: MimeType -> Set Extension
|
||||||
|
mimeExtensions needle = Set.fromList [ ext | (ext, typ) <- Map.toList mimeMap, typ == needle ]
|
||||||
|
|
||||||
|
archiveTypes :: Set MimeType
|
||||||
|
archiveTypes = $(mimeSetFile "config/archive-types")
|
||||||
@ -1,5 +1,7 @@
|
|||||||
module UnliftIO.Async.Utils
|
module UnliftIO.Async.Utils
|
||||||
( allocateAsync, allocateLinkedAsync
|
( allocateAsync, allocateLinkedAsync
|
||||||
|
, allocateAsyncWithUnmask, allocateLinkedAsyncWithUnmask
|
||||||
|
, allocateAsyncMasked, allocateLinkedAsyncMasked
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude hiding (cancel, async, link)
|
import ClassyPrelude hiding (cancel, async, link)
|
||||||
@ -17,3 +19,21 @@ allocateAsync = fmap (view _2) . flip allocate cancel . liftIO . async
|
|||||||
|
|
||||||
allocateLinkedAsync :: forall m a. (MonadUnliftIO m, MonadResource m) => IO a -> m (Async a)
|
allocateLinkedAsync :: forall m a. (MonadUnliftIO m, MonadResource m) => IO a -> m (Async a)
|
||||||
allocateLinkedAsync = uncurry (<$) . (id &&& link) <=< allocateAsync
|
allocateLinkedAsync = uncurry (<$) . (id &&& link) <=< allocateAsync
|
||||||
|
|
||||||
|
|
||||||
|
allocateAsyncWithUnmask :: forall m a.
|
||||||
|
MonadResource m
|
||||||
|
=> ((forall b. IO b -> IO b) -> IO a) -> m (Async a)
|
||||||
|
allocateAsyncWithUnmask act = fmap (view _2) . flip allocate cancel . liftIO $ asyncWithUnmask act
|
||||||
|
|
||||||
|
allocateLinkedAsyncWithUnmask :: forall m a. (MonadUnliftIO m, MonadResource m) => ((forall b. IO b -> IO b) -> IO a) -> m (Async a)
|
||||||
|
allocateLinkedAsyncWithUnmask act = uncurry (<$) . (id &&& link) =<< allocateAsyncWithUnmask act
|
||||||
|
|
||||||
|
|
||||||
|
allocateAsyncMasked :: forall m a.
|
||||||
|
MonadResource m
|
||||||
|
=> IO a -> m (Async a)
|
||||||
|
allocateAsyncMasked act = fmap (view _2) . flip allocate cancel . liftIO $ asyncWithUnmask (const act)
|
||||||
|
|
||||||
|
allocateLinkedAsyncMasked :: forall m a. (MonadUnliftIO m, MonadResource m) => IO a -> m (Async a)
|
||||||
|
allocateLinkedAsyncMasked = uncurry (<$) . (id &&& link) <=< allocateAsyncMasked
|
||||||
|
|||||||
19
src/Utils.hs
19
src/Utils.hs
@ -85,6 +85,9 @@ import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, Bounded
|
|||||||
|
|
||||||
import Data.Constraint (Dict(..))
|
import Data.Constraint (Dict(..))
|
||||||
|
|
||||||
|
import Control.Monad.Random.Class (MonadRandom)
|
||||||
|
import qualified System.Random.Shuffle as Rand (shuffleM)
|
||||||
|
|
||||||
{-# ANN module ("HLint: ignore Use asum" :: String) #-}
|
{-# ANN module ("HLint: ignore Use asum" :: String) #-}
|
||||||
|
|
||||||
|
|
||||||
@ -409,7 +412,8 @@ mapSymmDiff a b = Map.fromListWith Set.union . map (over _2 Set.singleton) . Set
|
|||||||
assocsSet :: Ord (k, v) => Map k v -> Set (k, v)
|
assocsSet :: Ord (k, v) => Map k v -> Set (k, v)
|
||||||
assocsSet = setOf folded . imap (,)
|
assocsSet = setOf folded . imap (,)
|
||||||
|
|
||||||
|
mapF :: (Ord k, Finite k) => (k -> v) -> Map k v
|
||||||
|
mapF = flip Map.fromSet $ Set.fromList universeF
|
||||||
|
|
||||||
---------------
|
---------------
|
||||||
-- Functions --
|
-- Functions --
|
||||||
@ -953,3 +957,16 @@ clampMin, clampMax :: Ord a
|
|||||||
-> a -- ^ Clamped Value
|
-> a -- ^ Clamped Value
|
||||||
clampMin = max
|
clampMin = max
|
||||||
clampMax = min
|
clampMax = min
|
||||||
|
|
||||||
|
------------
|
||||||
|
-- Random --
|
||||||
|
------------
|
||||||
|
|
||||||
|
unstableSortBy :: MonadRandom m => (a -> a -> Ordering) -> [a] -> m [a]
|
||||||
|
unstableSortBy cmp = fmap concat . mapM Rand.shuffleM . groupBy (\a b -> cmp a b == EQ) . sortBy cmp
|
||||||
|
|
||||||
|
unstableSortOn :: (MonadRandom m, Ord b) => (a -> b) -> [a] -> m [a]
|
||||||
|
unstableSortOn = unstableSortBy . comparing
|
||||||
|
|
||||||
|
unstableSort :: (MonadRandom m, Ord a) => [a] -> m [a]
|
||||||
|
unstableSort = unstableSortBy compare
|
||||||
|
|||||||
@ -1,14 +1,39 @@
|
|||||||
|
{-# OPTIONS -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Utils.Csv
|
module Utils.Csv
|
||||||
( pathPieceCsv
|
( typeCsv, typeCsv', extensionCsv
|
||||||
|
, pathPieceCsv
|
||||||
, (.:??)
|
, (.:??)
|
||||||
|
, CsvRendered(..)
|
||||||
|
, toCsvRendered
|
||||||
|
, toDefaultOrderedCsvRendered
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import ClassyPrelude hiding (lookup)
|
import ClassyPrelude hiding (lookup)
|
||||||
|
import Settings.Mime
|
||||||
|
|
||||||
import Data.Csv hiding (Name)
|
import Data.Csv hiding (Name)
|
||||||
|
import Data.Csv.Conduit (CsvParseError)
|
||||||
|
|
||||||
import Language.Haskell.TH (Name)
|
import Language.Haskell.TH (Name)
|
||||||
import Language.Haskell.TH.Lib
|
import Language.Haskell.TH.Lib
|
||||||
|
|
||||||
|
import Yesod.Core.Content (ContentType, simpleContentType)
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
|
deriving instance Typeable CsvParseError
|
||||||
|
instance Exception CsvParseError
|
||||||
|
|
||||||
|
|
||||||
|
typeCsv, typeCsv' :: ContentType
|
||||||
|
typeCsv = simpleContentType typeCsv'
|
||||||
|
typeCsv' = "text/csv; charset=UTF-8; header=present"
|
||||||
|
|
||||||
|
extensionCsv :: Extension
|
||||||
|
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
|
||||||
|
|
||||||
|
|
||||||
pathPieceCsv :: Name -> DecsQ
|
pathPieceCsv :: Name -> DecsQ
|
||||||
pathPieceCsv (conT -> t) =
|
pathPieceCsv (conT -> t) =
|
||||||
@ -22,3 +47,27 @@ pathPieceCsv (conT -> t) =
|
|||||||
|
|
||||||
(.:??) :: FromField (Maybe a) => NamedRecord -> ByteString -> Parser (Maybe a)
|
(.:??) :: FromField (Maybe a) => NamedRecord -> ByteString -> Parser (Maybe a)
|
||||||
m .:?? name = lookup m name <|> return Nothing
|
m .:?? name = lookup m name <|> return Nothing
|
||||||
|
|
||||||
|
|
||||||
|
data CsvRendered = CsvRendered
|
||||||
|
{ csvRenderedHeader :: Header
|
||||||
|
, csvRenderedData :: [NamedRecord]
|
||||||
|
} deriving (Eq, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
toCsvRendered :: forall mono.
|
||||||
|
( ToNamedRecord (Element mono)
|
||||||
|
, MonoFoldable mono
|
||||||
|
)
|
||||||
|
=> Header
|
||||||
|
-> mono -> CsvRendered
|
||||||
|
toCsvRendered csvRenderedHeader (otoList -> csvs) = CsvRendered{..}
|
||||||
|
where
|
||||||
|
csvRenderedData = map toNamedRecord csvs
|
||||||
|
|
||||||
|
toDefaultOrderedCsvRendered :: forall mono.
|
||||||
|
( ToNamedRecord (Element mono)
|
||||||
|
, DefaultOrdered (Element mono)
|
||||||
|
, MonoFoldable mono
|
||||||
|
)
|
||||||
|
=> mono -> CsvRendered
|
||||||
|
toDefaultOrderedCsvRendered = toCsvRendered $ headerOrder (error "headerOrder" :: Element mono)
|
||||||
|
|||||||
@ -328,7 +328,7 @@ combinedButtonField :: forall a m.
|
|||||||
) => [a] -> FieldSettings (HandlerSite m) -> AForm m [Maybe a]
|
) => [a] -> FieldSettings (HandlerSite m) -> AForm m [Maybe a]
|
||||||
combinedButtonField bs FieldSettings{..} = formToAForm $ do
|
combinedButtonField bs FieldSettings{..} = formToAForm $ do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
fvId <- maybe newFormIdent return fsId
|
fvId <- maybe newIdent return fsId
|
||||||
name <- maybe newFormIdent return fsName
|
name <- maybe newFormIdent return fsName
|
||||||
(ress, fvs) <- fmap unzip . for bs $ \b -> mopt (buttonField b) ("" { fsId = Just $ fvId <> "__" <> toPathPiece b
|
(ress, fvs) <- fmap unzip . for bs $ \b -> mopt (buttonField b) ("" { fsId = Just $ fvId <> "__" <> toPathPiece b
|
||||||
, fsName = Just $ name <> "__" <> toPathPiece b
|
, fsName = Just $ name <> "__" <> toPathPiece b
|
||||||
@ -491,6 +491,24 @@ reorderField optList = Field{..}
|
|||||||
withNum t n = tshow n <> "." <> t
|
withNum t n = tshow n <> "." <> t
|
||||||
$(widgetFile "widgets/permutation/permutation")
|
$(widgetFile "widgets/permutation/permutation")
|
||||||
|
|
||||||
|
optionsPathPiece :: ( MonadHandler m
|
||||||
|
, HandlerSite m ~ site
|
||||||
|
, MonoFoldable mono
|
||||||
|
, Element mono ~ (msg, val)
|
||||||
|
, RenderMessage site msg
|
||||||
|
, PathPiece val
|
||||||
|
)
|
||||||
|
=> mono -> m (OptionList val)
|
||||||
|
optionsPathPiece (otoList -> opts) = do
|
||||||
|
mr <- getMessageRender
|
||||||
|
let
|
||||||
|
mkOption (m, a) = Option
|
||||||
|
{ optionDisplay = mr m
|
||||||
|
, optionInternalValue = a
|
||||||
|
, optionExternalValue = toPathPiece a
|
||||||
|
}
|
||||||
|
return . mkOptionList $ mkOption <$> opts
|
||||||
|
|
||||||
optionsF :: ( MonadHandler m
|
optionsF :: ( MonadHandler m
|
||||||
, RenderMessage site (Element mono)
|
, RenderMessage site (Element mono)
|
||||||
, HandlerSite m ~ site
|
, HandlerSite m ~ site
|
||||||
@ -498,16 +516,7 @@ optionsF :: ( MonadHandler m
|
|||||||
, MonoFoldable mono
|
, MonoFoldable mono
|
||||||
)
|
)
|
||||||
=> mono -> m (OptionList (Element mono))
|
=> mono -> m (OptionList (Element mono))
|
||||||
optionsF (otoList -> opts) = do
|
optionsF = optionsPathPiece . map (id &&& id) . otoList
|
||||||
mr <- getMessageRender
|
|
||||||
let
|
|
||||||
mkOption a = Option
|
|
||||||
{ optionDisplay = mr a
|
|
||||||
, optionInternalValue = a
|
|
||||||
, optionExternalValue = toPathPiece a
|
|
||||||
}
|
|
||||||
return . mkOptionList $ mkOption <$> opts
|
|
||||||
|
|
||||||
|
|
||||||
optionsFinite :: ( MonadHandler m
|
optionsFinite :: ( MonadHandler m
|
||||||
, Finite a
|
, Finite a
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import ClassyPrelude.Yesod
|
|||||||
import Database.PostgreSQL.Simple (SqlError(SqlError), sqlErrorHint)
|
import Database.PostgreSQL.Simple (SqlError(SqlError), sqlErrorHint)
|
||||||
import Control.Monad.Catch (MonadMask)
|
import Control.Monad.Catch (MonadMask)
|
||||||
|
|
||||||
|
import Database.Persist.Sql
|
||||||
import Database.Persist.Sql.Raw.QQ
|
import Database.Persist.Sql.Raw.QQ
|
||||||
|
|
||||||
import Control.Retry
|
import Control.Retry
|
||||||
@ -14,20 +15,22 @@ import Control.Retry
|
|||||||
import Control.Lens ((&))
|
import Control.Lens ((&))
|
||||||
|
|
||||||
|
|
||||||
retryTransaction :: forall m a. (MonadLogger m, MonadMask m, MonadIO m) => m a -> m a
|
setSerializable :: forall m a. (MonadLogger m, MonadMask m, MonadIO m) => ReaderT SqlBackend m a -> ReaderT SqlBackend m a
|
||||||
retryTransaction = recovering policy [logRetries suggestRetry logRetry] . const
|
setSerializable act = recovering policy [logRetries suggestRetry logRetry] act'
|
||||||
where
|
where
|
||||||
policy :: RetryPolicyM m
|
policy :: RetryPolicyM (ReaderT SqlBackend m)
|
||||||
policy = fullJitterBackoff 1e3 & limitRetriesByCumulativeDelay 10e6
|
policy = fullJitterBackoff 1e3 & limitRetriesByCumulativeDelay 10e6
|
||||||
|
|
||||||
suggestRetry :: SqlError -> m Bool
|
suggestRetry :: SqlError -> ReaderT SqlBackend m Bool
|
||||||
suggestRetry SqlError{sqlErrorHint} = return $ "The transaction might succeed if retried." `isInfixOf` sqlErrorHint
|
suggestRetry SqlError{sqlErrorHint} = return $ "The transaction might succeed if retried." `isInfixOf` sqlErrorHint
|
||||||
|
|
||||||
logRetry :: Bool -- ^ Will retry
|
logRetry :: Bool -- ^ Will retry
|
||||||
-> SqlError
|
-> SqlError
|
||||||
-> RetryStatus
|
-> RetryStatus
|
||||||
-> m ()
|
-> ReaderT SqlBackend m ()
|
||||||
logRetry shouldRetry err status = $logDebugS "Sql" . pack $ defaultLogMsg shouldRetry err status
|
logRetry shouldRetry err status = $logDebugS "Sql" . pack $ defaultLogMsg shouldRetry err status
|
||||||
|
|
||||||
setSerializable :: (MonadLogger m, MonadMask m, MonadIO m) => ReaderT SqlBackend m a -> ReaderT SqlBackend m a
|
act' :: RetryStatus -> ReaderT SqlBackend m a
|
||||||
setSerializable act = retryTransaction $ [executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *> act
|
act' RetryStatus{..}
|
||||||
|
| rsIterNumber == 0 = [executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *> act
|
||||||
|
| otherwise = transactionUndoWithIsolation Serializable *> act
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import Control.Monad.Trans.Reader (ReaderT, mapReaderT, runReaderT)
|
|||||||
import Control.Monad.Base (MonadBase)
|
import Control.Monad.Base (MonadBase)
|
||||||
import Control.Monad.Trans.Control (MonadBaseControl)
|
import Control.Monad.Trans.Control (MonadBaseControl)
|
||||||
import Control.Monad.Catch (MonadMask, MonadCatch)
|
import Control.Monad.Catch (MonadMask, MonadCatch)
|
||||||
|
import Control.Monad.Random.Class (MonadRandom)
|
||||||
|
import Control.Monad.Morph (MFunctor, MMonad)
|
||||||
|
|
||||||
|
|
||||||
deriving via (ReaderT (HandlerData site site) IO) instance MonadFix (HandlerFor site)
|
deriving via (ReaderT (HandlerData site site) IO) instance MonadFix (HandlerFor site)
|
||||||
@ -40,6 +42,9 @@ deriving via (ReaderT (WidgetData site) IO) instance MonadMask (WidgetFor site)
|
|||||||
deriving via (ReaderT (HandlerData site site) IO) instance MonadBase IO (HandlerFor site)
|
deriving via (ReaderT (HandlerData site site) IO) instance MonadBase IO (HandlerFor site)
|
||||||
deriving via (ReaderT (WidgetData site) IO) instance MonadBase IO (WidgetFor site)
|
deriving via (ReaderT (WidgetData site) IO) instance MonadBase IO (WidgetFor site)
|
||||||
|
|
||||||
|
deriving via (ReaderT (HandlerData site site) IO) instance MonadRandom (HandlerFor site)
|
||||||
|
deriving via (ReaderT (WidgetData site) IO) instance MonadRandom (WidgetFor site)
|
||||||
|
|
||||||
|
|
||||||
-- | Type-level tags for compatability of Yesod `cached`-System with `MonadMemo`
|
-- | Type-level tags for compatability of Yesod `cached`-System with `MonadMemo`
|
||||||
newtype CachedMemoT k v m a = CachedMemoT { runCachedMemoT' :: ReaderT Loc m a }
|
newtype CachedMemoT k v m a = CachedMemoT { runCachedMemoT' :: ReaderT Loc m a }
|
||||||
@ -48,6 +53,7 @@ newtype CachedMemoT k v m a = CachedMemoT { runCachedMemoT' :: ReaderT Loc m a }
|
|||||||
, MonadThrow, MonadCatch, MonadMask, MonadLogger, MonadLoggerIO
|
, MonadThrow, MonadCatch, MonadMask, MonadLogger, MonadLoggerIO
|
||||||
, MonadResource, MonadHandler, MonadWidget
|
, MonadResource, MonadHandler, MonadWidget
|
||||||
)
|
)
|
||||||
|
deriving newtype ( MFunctor, MMonad, MonadTrans )
|
||||||
|
|
||||||
deriving newtype instance MonadBase b m => MonadBase b (CachedMemoT k v m)
|
deriving newtype instance MonadBase b m => MonadBase b (CachedMemoT k v m)
|
||||||
deriving newtype instance MonadBaseControl b m => MonadBaseControl b (CachedMemoT k v m)
|
deriving newtype instance MonadBaseControl b m => MonadBaseControl b (CachedMemoT k v m)
|
||||||
@ -56,7 +62,8 @@ instance MonadReader r m => MonadReader r (CachedMemoT k v m) where
|
|||||||
reader = CachedMemoT . lift . reader
|
reader = CachedMemoT . lift . reader
|
||||||
local f (CachedMemoT act) = CachedMemoT $ mapReaderT (local f) act
|
local f (CachedMemoT act) = CachedMemoT $ mapReaderT (local f) act
|
||||||
|
|
||||||
deriving via (ReaderT Loc) instance MonadTrans (CachedMemoT k v)
|
instance MonadUnliftIO m => MonadUnliftIO (CachedMemoT k v m) where
|
||||||
|
askUnliftIO = (\UnliftIO{..} -> UnliftIO $ \(CachedMemoT f) -> unliftIO f) <$> CachedMemoT askUnliftIO
|
||||||
|
|
||||||
|
|
||||||
-- | Uses `cachedBy` with a `Binary`-encoded @k@
|
-- | Uses `cachedBy` with a `Binary`-encoded @k@
|
||||||
@ -69,3 +76,7 @@ runCachedMemoT :: Q Exp
|
|||||||
runCachedMemoT = do
|
runCachedMemoT = do
|
||||||
loc <- location
|
loc <- location
|
||||||
[e| flip runReaderT loc . runCachedMemoT' |]
|
[e| flip runReaderT loc . runCachedMemoT' |]
|
||||||
|
|
||||||
|
|
||||||
|
instance site ~ site' => ToWidget site (SomeMessage site') where
|
||||||
|
toWidget msg = toWidget =<< (getMessageRender <*> pure msg)
|
||||||
|
|||||||
@ -56,5 +56,7 @@ extra-deps:
|
|||||||
|
|
||||||
- persistent-qq-2.9.1
|
- persistent-qq-2.9.1
|
||||||
|
|
||||||
|
- process-1.6.5.1
|
||||||
|
|
||||||
resolver: lts-13.21
|
resolver: lts-13.21
|
||||||
allow-newer: true
|
allow-newer: true
|
||||||
|
|||||||
@ -1,22 +1,28 @@
|
|||||||
$newline never
|
$newline never
|
||||||
$if not (null allocationsBounds)
|
$if not (null allocationsBounds)
|
||||||
<h2>_{MsgCourseAllocationsBounds (length allocationsBounds)}
|
<section>
|
||||||
<dl .deflist>
|
<h2>_{MsgCourseAllocationsBounds (length allocationsBounds)}
|
||||||
$forall (Allocation{allocationName, allocationRegisterTo}, numApps, numFirstChoice, capped) <- allocationsBounds
|
<dl .deflist>
|
||||||
<dt .deflist__dt>
|
$forall (Allocation{allocationName, allocationRegisterTo}, numApps, numFirstChoice, capped) <- allocationsBounds
|
||||||
#{allocationName}
|
<dt .deflist__dt>
|
||||||
<dd .deflist__dd>
|
#{allocationName}
|
||||||
<p>
|
<dd .deflist__dd>
|
||||||
$if numApps == numFirstChoice
|
<p>
|
||||||
_{MsgCourseAllocationsBoundCoincide numFirstChoice}
|
$if numApps == numFirstChoice
|
||||||
$else
|
_{MsgCourseAllocationsBoundCoincide numFirstChoice}
|
||||||
_{MsgCourseAllocationsBound numApps numFirstChoice}
|
$else
|
||||||
$if capped
|
_{MsgCourseAllocationsBound numApps numFirstChoice}
|
||||||
<p .bound_explanation>
|
$if capped
|
||||||
_{MsgCourseAllocationsBoundCapped}
|
<p .bound_explanation>
|
||||||
$if registrationOpen allocationRegisterTo
|
_{MsgCourseAllocationsBoundCapped}
|
||||||
<p .bound_explanation>
|
$if registrationOpen allocationRegisterTo
|
||||||
_{MsgCourseAllocationsBoundWarningOpen}
|
<p .bound_explanation>
|
||||||
|
_{MsgCourseAllocationsBoundWarningOpen}
|
||||||
|
$if mayAccept
|
||||||
|
<section>
|
||||||
|
<p>_{MsgBtnAcceptApplicationsTip}
|
||||||
|
^{acceptWgt}
|
||||||
|
|
||||||
<h2>_{MsgMenuCourseApplications}
|
<section>
|
||||||
^{table}
|
<h2>_{MsgMenuCourseApplications}
|
||||||
|
^{table}
|
||||||
|
|||||||
@ -114,7 +114,7 @@ $if not (null occurrences)
|
|||||||
<th .table__th>_{MsgExamRoomDescription}
|
<th .table__th>_{MsgExamRoomDescription}
|
||||||
<tbody>
|
<tbody>
|
||||||
$forall (Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences
|
$forall (Entity _occId ExamOccurrence{examOccurrenceName, examOccurrenceRoom, examOccurrenceStart, examOccurrenceEnd, examOccurrenceDescription}, registered) <- occurrences
|
||||||
<tr .table__row :occurrenceAssignmentsShown && not registered:.occurrence--not-registered>
|
<tr .table__row :occurrenceAssignmentsShown && (not registered && hasRegistration):.occurrence--not-registered>
|
||||||
$if occurrenceNamesShown
|
$if occurrenceNamesShown
|
||||||
<td .table__td #exam-occurrence__#{examOccurrenceName}>#{examOccurrenceName}
|
<td .table__td #exam-occurrence__#{examOccurrenceName}>#{examOccurrenceName}
|
||||||
$if occurrenceAssignmentsShown
|
$if occurrenceAssignmentsShown
|
||||||
|
|||||||
@ -1,5 +1,20 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>
|
||||||
|
^{formatGregorianW 2019 09 27}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Automatische Anmeldung von Bewerbern in Kursen, die nicht an einer Zentralanmeldung teilnehmen (nach Bewertung der Bewerbung)
|
||||||
|
|
||||||
|
<dt .deflist__dt>
|
||||||
|
^{formatGregorianW 2019 09 25}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>Automatische Berechnung von Prufüngsboni
|
||||||
|
<li>Automatische Berechnung von Prüfungsleistungen
|
||||||
|
<li><i>Bugfix</i>: Uhrzeiten werden beim Laden eines Formulars nichtmehr zurückgesetzt
|
||||||
|
<li><i>Bugfix</i>: Studierende tauchen in der Prüfungsleistungen-Tabelle nicht mehr mehrfach auf
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
^{formatGregorianW 2019 09 16}
|
^{formatGregorianW 2019 09 16}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
|
|||||||
@ -1,12 +1,23 @@
|
|||||||
|
$newline never
|
||||||
<h3>Hinweise zum Import von CSV-Dateien
|
<h3>Hinweise zum Import von CSV-Dateien
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>Datenformat
|
||||||
|
<dd .deflist__dd>
|
||||||
|
Beim Import wird, pro Spalte, das selbe Datenformat erwartet, wie es beim #
|
||||||
|
Export produziert wird (siehe <i>Spalten- & Zellenformat</i> #
|
||||||
|
unter <i>CSV-Export</i>).<br />
|
||||||
|
Spalten können beliebig permutiert werden und dürfen auch fehlen (in #
|
||||||
|
diesem Fall wird die fehlende Spalte so behandelt als enthielte sie in #
|
||||||
|
jeder Zeile eine leere Zelle).<br />
|
||||||
|
Spalten werden an ihrer Überschrift identifiziert. #
|
||||||
|
Die Überschrift darf daher nicht verändert oder entfernt werden.
|
||||||
<dt .deflist__dt>Änderungen
|
<dt .deflist__dt>Änderungen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Einige Zellen können durch den Import verändert werden.
|
Einige Zellen können durch den Import verändert werden.<br />
|
||||||
Nicht-änderbare Zellen werden ignoriert, falls diese verändert wurden.
|
Nicht-änderbare Zellen werden ignoriert, falls diese verändert wurden.
|
||||||
<dt .deflist__dt>Vorschau
|
<dt .deflist__dt>Vorschau
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Es wird eine Vorschau angezeigt, bevor irgendetwas tatsächlich geändert wird.
|
Es wird eine Vorschau angezeigt, bevor irgendetwas tatsächlich geändert wird.<br />
|
||||||
In der Vorschau können dann auch nur teilweise Änderungen ausgewählt werden.
|
In der Vorschau können dann auch nur teilweise Änderungen ausgewählt werden.
|
||||||
<dt .deflist__dt>Leere Zellen
|
<dt .deflist__dt>Leere Zellen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
@ -16,22 +27,22 @@
|
|||||||
<p>
|
<p>
|
||||||
Es werden nur konsistente Änderungen akzeptiert!
|
Es werden nur konsistente Änderungen akzeptiert!
|
||||||
<p>
|
<p>
|
||||||
Daraus folgt, dass es sinnvoll sein kann, gewisse Zellen frei zu lassen;
|
Daraus folgt, dass es sinnvoll sein kann, gewisse Zellen frei zu lassen; #
|
||||||
z.B. ändert man ein Studienfachzuordnung eines Teilnehmers ab,
|
ändert man z.B. die Studienfachzuordnung eines Teilnehmers ab, #
|
||||||
dann müsste man auch Abschluss und Semesterzahl passend ändern.
|
so müsste man auch Abschluss und Fachsemester passend ändern.<br />
|
||||||
Da diese jedoch eindeutig sind, kann man diese Zellen einfach frei lassen.
|
Da diese jedoch eindeutig sind, kann man diese Zellen einfach frei lassen.
|
||||||
<dt .deflist__dt>Zeilen Identifikation
|
<dt .deflist__dt>Zeilen Identifikation
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Mehrere Spalten werden zur Identifikation der Zeile verwendet.
|
Mehrere Spalten werden zur Identifikation der Zeile verwendet.<br />
|
||||||
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden sein,
|
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden sein, #
|
||||||
so lange die Identifikation noch eindeutig ist.
|
so lange die Identifikation noch eindeutig ist.<br />
|
||||||
Sind mehrere Werte vorhanden, so müssen diese natürlich zueinander passen.
|
Sind mehrere Werte vorhanden, so müssen diese natürlich zueinander passen.
|
||||||
<dt .deflist__dt>Zeilen hinzufügen
|
<dt .deflist__dt>Zeilen hinzufügen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Es können auch neue Zeilen hinzugefügt werden, so fern ausreichend
|
Es können auch neue Zeilen hinzugefügt werden, sofern ausreichend #
|
||||||
eindeutige Informationen vorhanden sind;
|
eindeutige Informationen vorhanden sind; #
|
||||||
z.B. können so Prüfungsteilnehmer nachgemeldet werden.
|
z.B. können so Prüfungsteilnehmer nachgemeldet werden.
|
||||||
<dt .deflist__dt>Zeilen löschen
|
<dt .deflist__dt>Zeilen löschen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Fehlende Zeilen werden in der Vorschau zur Löschung angeboten
|
Fehlende Zeilen werden in der Vorschau zur Löschung angeboten #
|
||||||
und dann ggf. gelöscht.
|
und dann ggf. gelöscht.
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<h2>
|
<h2>
|
||||||
_{MsgCourseParticipantsAlreadyRegistered (length aurAlreadyRegistered)}
|
_{MsgCourseParticipantsAlreadyRegistered (length aurAlreadyRegistered)}
|
||||||
<ul>
|
<ul>
|
||||||
$forall email <- aurAlreadyRegistered
|
$forall email <- aurAlreadyRegistered'
|
||||||
<li style="font-family: monospace">#{email}
|
<li style="font-family: monospace">#{email}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<h2>
|
<h2>
|
||||||
_{MsgCourseParticipantsRegisteredWithoutField (length aurNoUniquePrimaryField)}
|
_{MsgCourseParticipantsRegisteredWithoutField (length aurNoUniquePrimaryField)}
|
||||||
<ul>
|
<ul>
|
||||||
$forall email <- aurNoUniquePrimaryField
|
$forall email <- aurNoUniquePrimaryField'
|
||||||
<li style="font-family: monospace">#{email}
|
<li style="font-family: monospace">#{email}
|
||||||
|
|||||||
@ -14,5 +14,7 @@ $if is _Just dbtCsvEncode
|
|||||||
<div .csv-export__content>
|
<div .csv-export__content>
|
||||||
<p>
|
<p>
|
||||||
^{csvColExplanations'}
|
^{csvColExplanations'}
|
||||||
|
<p>
|
||||||
|
^{modal (i18n MsgCsvChangeOptionsLabel) (Left (SomeRoute CsvOptionsR))}
|
||||||
^{csvExportWdgt'}
|
^{csvExportWdgt'}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
$newline never
|
$newline never
|
||||||
$case bonusRule
|
$case bonusRule
|
||||||
|
$of ExamBonusManual _
|
||||||
|
_{MsgExamBonusManualParticipants}
|
||||||
$of ExamBonusPoints ps False _
|
$of ExamBonusPoints ps False _
|
||||||
_{MsgExamBonusPoints ps}
|
_{MsgExamBonusPoints ps}
|
||||||
$of ExamBonusPoints ps True _
|
$of ExamBonusPoints ps True _
|
||||||
|
|||||||
2
test.sh
2
test.sh
@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
[[ -n "${FORCE_RELEASE}" ]] && exit 0
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|||||||
@ -111,6 +111,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = csvPreset # CsvPresetRFC
|
||||||
}
|
}
|
||||||
fhamann <- insert User
|
fhamann <- insert User
|
||||||
{ userIdent = "felix.hamann@campus.lmu.de"
|
{ userIdent = "felix.hamann@campus.lmu.de"
|
||||||
@ -135,6 +136,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = csvPreset # CsvPresetExcel
|
||||||
}
|
}
|
||||||
jost <- insert User
|
jost <- insert User
|
||||||
{ userIdent = "jost@tcs.ifi.lmu.de"
|
{ userIdent = "jost@tcs.ifi.lmu.de"
|
||||||
@ -159,6 +161,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = def
|
||||||
}
|
}
|
||||||
maxMuster <- insert User
|
maxMuster <- insert User
|
||||||
{ userIdent = "max@campus.lmu.de"
|
{ userIdent = "max@campus.lmu.de"
|
||||||
@ -183,6 +186,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = def
|
||||||
}
|
}
|
||||||
tinaTester <- insert $ User
|
tinaTester <- insert $ User
|
||||||
{ userIdent = "tester@campus.lmu.de"
|
{ userIdent = "tester@campus.lmu.de"
|
||||||
@ -207,6 +211,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = def
|
||||||
}
|
}
|
||||||
svaupel <- insert User
|
svaupel <- insert User
|
||||||
{ userIdent = "vaupel.sarah@campus.lmu.de"
|
{ userIdent = "vaupel.sarah@campus.lmu.de"
|
||||||
@ -231,6 +236,7 @@ fillDb = do
|
|||||||
, userNotificationSettings = def
|
, userNotificationSettings = def
|
||||||
, userCreated = now
|
, userCreated = now
|
||||||
, userLastLdapSynchronisation = Nothing
|
, userLastLdapSynchronisation = Nothing
|
||||||
|
, userCsvOptions = def
|
||||||
}
|
}
|
||||||
void . repsert (TermKey summer2017) $ Term
|
void . repsert (TermKey summer2017) $ Term
|
||||||
{ termName = summer2017
|
{ termName = summer2017
|
||||||
|
|||||||
@ -18,10 +18,11 @@ instance Arbitrary (Route Auth) where
|
|||||||
instance Arbitrary (Route EmbeddedStatic) where
|
instance Arbitrary (Route EmbeddedStatic) where
|
||||||
arbitrary = do
|
arbitrary = do
|
||||||
let printableText = pack . filter (/= '/') . getPrintableString <$> arbitrary
|
let printableText = pack . filter (/= '/') . getPrintableString <$> arbitrary
|
||||||
|
printableText' = printableText `suchThat` (not . null)
|
||||||
pathLength <- getPositive <$> arbitrary
|
pathLength <- getPositive <$> arbitrary
|
||||||
path <- replicateM pathLength printableText
|
path <- replicateM pathLength printableText'
|
||||||
paramNum <- getNonNegative <$> arbitrary
|
paramNum <- getNonNegative <$> arbitrary
|
||||||
params <- replicateM paramNum $ (,) <$> printableText <*> printableText
|
params <- replicateM paramNum $ (,) <$> printableText' <*> printableText
|
||||||
return $ embeddedResourceR path params
|
return $ embeddedResourceR path params
|
||||||
|
|
||||||
instance Arbitrary SchoolR where
|
instance Arbitrary SchoolR where
|
||||||
|
|||||||
12
test/Model/MigrationSpec.hs
Normal file
12
test/Model/MigrationSpec.hs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module Model.MigrationSpec where
|
||||||
|
|
||||||
|
import TestImport
|
||||||
|
|
||||||
|
import Model.Migration
|
||||||
|
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec = withApp $ -- `withApp` does migration, if needed
|
||||||
|
describe "Migration" $
|
||||||
|
it "is idempotent" $
|
||||||
|
(`shouldBe` False) <$> runDB requiresMigration -- Migration shouldn't be needed after `withApp` above
|
||||||
@ -8,6 +8,7 @@ import Settings
|
|||||||
import Control.Lens (review, preview)
|
import Control.Lens (review, preview)
|
||||||
import Data.Aeson (Value)
|
import Data.Aeson (Value)
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
|
||||||
import MailSpec ()
|
import MailSpec ()
|
||||||
|
|
||||||
@ -32,6 +33,8 @@ import Data.Scientific
|
|||||||
|
|
||||||
import Utils.Lens
|
import Utils.Lens
|
||||||
|
|
||||||
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
|
|
||||||
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
instance (Arbitrary a, MonoFoldable a) => Arbitrary (NonNull a) where
|
||||||
arbitrary = arbitrary `suchThatMap` fromNullable
|
arbitrary = arbitrary `suchThatMap` fromNullable
|
||||||
@ -250,6 +253,28 @@ instance Arbitrary ExamPassed where
|
|||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary Quoting where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary CsvOptions where
|
||||||
|
arbitrary = CsvOptions
|
||||||
|
<$> suchThat arbitrary validDelimiter
|
||||||
|
<*> arbitrary
|
||||||
|
<*> arbitrary
|
||||||
|
where
|
||||||
|
validDelimiter c = and
|
||||||
|
[ Char.isLatin1 c
|
||||||
|
, c /= '"'
|
||||||
|
, c /= '\r'
|
||||||
|
, c /= '\n'
|
||||||
|
]
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary CsvPreset where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = do
|
spec = do
|
||||||
@ -334,6 +359,12 @@ spec = do
|
|||||||
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
||||||
lawsCheckHspec (Proxy @ExamPassed)
|
lawsCheckHspec (Proxy @ExamPassed)
|
||||||
[ eqLaws, ordLaws, showReadLaws, finiteLaws, jsonLaws, pathPieceLaws, persistFieldLaws, csvFieldLaws ]
|
[ eqLaws, ordLaws, showReadLaws, finiteLaws, jsonLaws, pathPieceLaws, persistFieldLaws, csvFieldLaws ]
|
||||||
|
lawsCheckHspec (Proxy @Quoting)
|
||||||
|
[ eqLaws, ordLaws, jsonLaws, showReadLaws, finiteLaws, pathPieceLaws ]
|
||||||
|
lawsCheckHspec (Proxy @CsvOptions)
|
||||||
|
[ eqLaws, ordLaws, showReadLaws, jsonLaws, persistFieldLaws ]
|
||||||
|
lawsCheckHspec (Proxy @CsvPreset)
|
||||||
|
[ eqLaws, ordLaws, showReadLaws, boundedEnumLaws, finiteLaws, pathPieceLaws ]
|
||||||
|
|
||||||
describe "TermIdentifier" $ do
|
describe "TermIdentifier" $ do
|
||||||
it "has compatible encoding/decoding to/from Text" . property $
|
it "has compatible encoding/decoding to/from Text" . property $
|
||||||
@ -365,6 +396,9 @@ spec = do
|
|||||||
parse "1.8" `shouldSatisfy` is _Left
|
parse "1.8" `shouldSatisfy` is _Left
|
||||||
parse "voided" `shouldBe` Right ExamVoided
|
parse "voided" `shouldBe` Right ExamVoided
|
||||||
parse "no-show" `shouldBe` Right ExamNoShow
|
parse "no-show" `shouldBe` Right ExamNoShow
|
||||||
|
describe "CsvOptions" $
|
||||||
|
it "json-decodes from empty object" . example $
|
||||||
|
Aeson.parseMaybe Aeson.parseJSON (Aeson.object []) `shouldBe` Just (def :: CsvOptions)
|
||||||
|
|
||||||
termExample :: (TermIdentifier, Text) -> Expectation
|
termExample :: (TermIdentifier, Text) -> Expectation
|
||||||
termExample (term, encoded) = example $ do
|
termExample (term, encoded) = example $ do
|
||||||
|
|||||||
@ -22,10 +22,13 @@ import Utils
|
|||||||
import System.FilePath
|
import System.FilePath
|
||||||
import Data.Time
|
import Data.Time
|
||||||
|
|
||||||
|
import Mail (MailLanguages(..))
|
||||||
|
|
||||||
|
|
||||||
instance Arbitrary EmailAddress where
|
instance Arbitrary EmailAddress where
|
||||||
arbitrary = do
|
arbitrary = do
|
||||||
local <- suchThat arbitrary (\l -> isEmail l (CBS.pack "example.com"))
|
local <- suchThat (CBS.pack . getPrintableString <$> arbitrary) (\l -> isEmail l (CBS.pack "example.com"))
|
||||||
domain <- suchThat arbitrary (\d -> isEmail (CBS.pack "example") d)
|
domain <- suchThat (CBS.pack . getPrintableString <$> arbitrary) (\d -> isEmail (CBS.pack "example") d)
|
||||||
let (Just result) = emailAddress (makeEmailLike local domain)
|
let (Just result) = emailAddress (makeEmailLike local domain)
|
||||||
pure result
|
pure result
|
||||||
|
|
||||||
@ -100,8 +103,9 @@ instance Arbitrary User where
|
|||||||
|
|
||||||
userDownloadFiles <- arbitrary
|
userDownloadFiles <- arbitrary
|
||||||
userWarningDays <- arbitrary
|
userWarningDays <- arbitrary
|
||||||
userMailLanguages <- arbitrary
|
userMailLanguages <- fmap MailLanguages $ sublistOf =<< shuffle (toList appLanguages)
|
||||||
userNotificationSettings <- arbitrary
|
userNotificationSettings <- arbitrary
|
||||||
|
userCsvOptions <- arbitrary
|
||||||
|
|
||||||
userCreated <- arbitrary
|
userCreated <- arbitrary
|
||||||
userLastLdapSynchronisation <- arbitrary
|
userLastLdapSynchronisation <- arbitrary
|
||||||
|
|||||||
@ -140,6 +140,7 @@ createUser adjUser = do
|
|||||||
userNotificationSettings = def
|
userNotificationSettings = def
|
||||||
userCreated = now
|
userCreated = now
|
||||||
userLastLdapSynchronisation = Nothing
|
userLastLdapSynchronisation = Nothing
|
||||||
|
userCsvOptions = def
|
||||||
runDB . insertEntity $ adjUser User{..}
|
runDB . insertEntity $ adjUser User{..}
|
||||||
|
|
||||||
lawsCheckHspec :: Typeable a => Proxy a -> [Proxy a -> Laws] -> Spec
|
lawsCheckHspec :: Typeable a => Proxy a -> [Proxy a -> Laws] -> Spec
|
||||||
|
|||||||
Reference in New Issue
Block a user