Merge branch 'master' into 476-interface-fur-klausurkorrekturen
This commit is contained in:
commit
36e90102c4
1
.gitignore
vendored
1
.gitignore
vendored
@ -38,4 +38,5 @@ test.log
|
|||||||
tunnel.log
|
tunnel.log
|
||||||
/static
|
/static
|
||||||
/well-known
|
/well-known
|
||||||
|
/.well-known-cache
|
||||||
/**/tmp-*
|
/**/tmp-*
|
||||||
|
|||||||
@ -6,6 +6,7 @@ default:
|
|||||||
- node_modules
|
- node_modules
|
||||||
- .stack
|
- .stack
|
||||||
- .stack-work
|
- .stack-work
|
||||||
|
- .well-known-cache
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
STACK_ROOT: "${CI_PROJECT_DIR}/.stack"
|
STACK_ROOT: "${CI_PROJECT_DIR}/.stack"
|
||||||
@ -13,6 +14,7 @@ variables:
|
|||||||
POSTGRES_DB: uniworx_test
|
POSTGRES_DB: uniworx_test
|
||||||
POSTGRES_USER: uniworx
|
POSTGRES_USER: uniworx
|
||||||
POSTGRES_PASSWORD: uniworx
|
POSTGRES_PASSWORD: uniworx
|
||||||
|
N_PREFIX: "${HOME}/.n"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- setup
|
- setup
|
||||||
@ -30,7 +32,8 @@ npm install:
|
|||||||
before_script: &npm
|
before_script: &npm
|
||||||
- apt-get update -y
|
- apt-get update -y
|
||||||
- npm install -g n
|
- npm install -g n
|
||||||
- n stable
|
- n 13.5.0
|
||||||
|
- export PATH="${N_PREFIX}/bin:$PATH"
|
||||||
- npm install -g npm
|
- npm install -g npm
|
||||||
- hash -r
|
- hash -r
|
||||||
- apt-get -y install openssh-client exiftool
|
- apt-get -y install openssh-client exiftool
|
||||||
@ -138,7 +141,8 @@ frontend:test:
|
|||||||
before_script:
|
before_script:
|
||||||
- apt-get update -y
|
- apt-get update -y
|
||||||
- npm install -g n
|
- npm install -g n
|
||||||
- n stable
|
- n 13.5.0
|
||||||
|
- export PATH="${N_PREFIX}/bin:$PATH"
|
||||||
- npm install -g npm
|
- npm install -g npm
|
||||||
- hash -r
|
- hash -r
|
||||||
- apt-get install -y --no-install-recommends chromium-browser
|
- apt-get install -y --no-install-recommends chromium-browser
|
||||||
|
|||||||
48
CHANGELOG.md
48
CHANGELOG.md
@ -2,6 +2,54 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
### [10.4.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.4.0...v10.4.1) (2020-01-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* hlint ([4348efc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4348efc))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [10.4.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.3.0...v10.4.0) (2020-01-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add missing translations ([d798dc4](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/d798dc4))
|
||||||
|
* improve csv import explanation ([729a8e8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/729a8e8))
|
||||||
|
* restrict guessUser to consistent queries ([bcd5326](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/bcd5326))
|
||||||
|
* tests & hlint ([4e9b618](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4e9b618))
|
||||||
|
* ui improvements for (external-)exams ([b3ce3dd](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b3ce3dd))
|
||||||
|
* **hide-columns:** bump storage manager minor version ([9053b87](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9053b87))
|
||||||
|
* **hide-columns:** no hide-columns in tail.datetime ([03bcf56](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03bcf56))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* course-participant-lists ([88dd5a9](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/88dd5a9))
|
||||||
|
* external exam csv export ([553c117](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/553c117))
|
||||||
|
* external exam csv import & ldap lookup during csv import ([1d14b6a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1d14b6a))
|
||||||
|
* external exams in exam office exams table ([3b739f7](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3b739f7))
|
||||||
|
* notification about externalExamResults to exam-office ([a304840](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/a304840))
|
||||||
|
* **external-exams:** auditing ([2b153c1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/2b153c1))
|
||||||
|
* **external-exams:** create new exams ([94bb391](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/94bb391))
|
||||||
|
* **external-exams:** display staff & add' schools ([c14d90f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/c14d90f))
|
||||||
|
* **external-exams:** edit existing exams ([1252a5f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1252a5f))
|
||||||
|
* **external-exams:** list ([fa3521d](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa3521d))
|
||||||
|
* **external-exams:** plan for student grade access ([b7506a0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b7506a0))
|
||||||
|
* **external-exams:** requisite routes ([f25b21a](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/f25b21a))
|
||||||
|
* **hide-columns:** add hider label th attr ([6c05a8f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/6c05a8f))
|
||||||
|
* **hide-columns:** add hider label th attr ([71e90a1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/71e90a1))
|
||||||
|
* **hide-columns:** add hider labels for material list ([ccafd95](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ccafd95))
|
||||||
|
* **hide-columns:** add hider labels for tutorial list on course page ([3553df2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3553df2))
|
||||||
|
* **hide-columns:** add hider labels for tutorial list on course page ([03e4ac1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/03e4ac1))
|
||||||
|
* **hide-columns:** add more hider labels ([555c4ae](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/555c4ae))
|
||||||
|
* **hide-columns:** add more hider labels ([eba58d8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/eba58d8))
|
||||||
|
* **hide-columns:** opt-out on select columns ([b03c10f](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b03c10f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [10.3.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.2.0...v10.3.0) (2020-01-12)
|
## [10.3.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v10.2.0...v10.3.0) (2020-01-12)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,5 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"html_code_file": true
|
"html_code_file": true
|
||||||
},
|
}
|
||||||
"versioning": false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -401,8 +401,10 @@ UnauthorizedToken404: Authorisierungs-Tokens können nicht auf Fehlerseiten ausg
|
|||||||
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
|
UnauthorizedSiteAdmin: Sie sind kein System-weiter Administrator.
|
||||||
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
|
UnauthorizedSchoolAdmin: Sie sind nicht als Administrator für dieses Institut eingetragen.
|
||||||
UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist.
|
UnauthorizedAdminEscalation: Sie sind nicht Administrator für alle Institute, für die dieser Nutzer Administrator oder Veranstalter ist.
|
||||||
UnauthorizedExamOffice: Sie sind nicht Teil eines Prüfungsamts.
|
UnauthorizedExamOffice: Sie sind nicht mit Prüfungsverwaltung beauftragt.
|
||||||
UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie Teil eines assoziierten Prüfungsamts sind.
|
UnauthorizedExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind.
|
||||||
|
UnauthorizedExternalExamExamOffice: Es existieren keine Prüfungsergebnisse für Nutzer, für die Sie mit der Prüfungsverwaltung beauftragt sind.
|
||||||
|
UnauthorizedEvaluation: Sie sind nicht mit der Kursumfragenverwaltung beauftragt.
|
||||||
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
|
UnauthorizedSchoolLecturer: Sie sind nicht als Veranstalter für dieses Institut eingetragen.
|
||||||
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
|
UnauthorizedLecturer: Sie sind nicht als Veranstalter für diese Veranstaltung eingetragen.
|
||||||
UnauthorizedAllocationLecturer: Sie sind nicht als Veranstalter für eine Veranstaltung dieser Zentralanmeldung eingetragen.
|
UnauthorizedAllocationLecturer: Sie sind nicht als Veranstalter für eine Veranstaltung dieser Zentralanmeldung eingetragen.
|
||||||
@ -414,6 +416,7 @@ UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung r
|
|||||||
UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert.
|
UnauthorizedAllocationRegistered: Sie sind nicht als Teilnehmer für diese Zentralanmeldung registriert.
|
||||||
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
|
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
|
||||||
UnauthorizedExamOccurrenceRegistration: Anmeldung zur Klausur erfolgt nicht inkl. Raum/Termin.
|
UnauthorizedExamOccurrenceRegistration: Anmeldung zur Klausur erfolgt nicht inkl. Raum/Termin.
|
||||||
|
UnauthorizedExternalExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
|
||||||
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
|
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
|
||||||
UnauthorizedParticipantSelf: Sie sind kein Teilnehmer dieser Veranstaltung.
|
UnauthorizedParticipantSelf: Sie sind kein Teilnehmer dieser Veranstaltung.
|
||||||
UnauthorizedApplicant: Angegebener Benutzer hat sich nicht für diese Veranstaltung beworben.
|
UnauthorizedApplicant: Angegebener Benutzer hat sich nicht für diese Veranstaltung beworben.
|
||||||
@ -450,6 +453,8 @@ UnauthorizedTutor: Sie sind nicht Tutor.
|
|||||||
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Tutorium mit derselben Registrierungs-Gruppe.
|
UnauthorizedTutorialRegisterGroup: Sie sind bereits in einem Tutorium mit derselben Registrierungs-Gruppe.
|
||||||
UnauthorizedLDAP: Angegebener Nutzer meldet sich nicht mit Campus-Kennung an.
|
UnauthorizedLDAP: Angegebener Nutzer meldet sich nicht mit Campus-Kennung an.
|
||||||
UnauthorizedPWHash: Angegebener Nutzer meldet sich nicht mit Uni2work-Kennung an.
|
UnauthorizedPWHash: Angegebener Nutzer meldet sich nicht mit Uni2work-Kennung an.
|
||||||
|
UnauthorizedExternalExamListNotEmpty: Liste von externen Prüfungen ist nicht leer
|
||||||
|
UnauthorizedExternalExamLecturer: Sie sind nicht als Prüfer für diese externe Prüfung eingetragen
|
||||||
|
|
||||||
UnauthorizedPasswordResetToken: Dieses Authorisierungs-Token kann nicht mehr zum Passwort ändern benutzt werden
|
UnauthorizedPasswordResetToken: Dieses Authorisierungs-Token kann nicht mehr zum Passwort ändern benutzt werden
|
||||||
|
|
||||||
@ -714,6 +719,7 @@ LoginNecessary: Bitte melden Sie sich dazu vorher an!
|
|||||||
|
|
||||||
InternalLdapError: Interner Fehler beim Campus-Login
|
InternalLdapError: Interner Fehler beim Campus-Login
|
||||||
|
|
||||||
|
CampusUserInvalidIdent: Konnte anhand des Campus-Logins keine eindeutige Identifikation
|
||||||
CampusUserInvalidEmail: Konnte anhand des Campus-Logins keine EMail-Addresse ermitteln
|
CampusUserInvalidEmail: Konnte anhand des Campus-Logins keine EMail-Addresse ermitteln
|
||||||
CampusUserInvalidDisplayName: Konnte anhand des Campus-Logins keinen vollen Namen ermitteln
|
CampusUserInvalidDisplayName: Konnte anhand des Campus-Logins keinen vollen Namen ermitteln
|
||||||
CampusUserInvalidGivenName: Konnte anhand des Campus-Logins keinen Vornamen ermitteln
|
CampusUserInvalidGivenName: Konnte anhand des Campus-Logins keinen Vornamen ermitteln
|
||||||
@ -862,6 +868,9 @@ MailExamOfficeExamResultsIntro courseName@Text termDesc@Text examn@ExamName: Ein
|
|||||||
MailSubjectExamOfficeExamResultsChanged csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden verändert
|
MailSubjectExamOfficeExamResultsChanged csh@CourseShorthand examn@ExamName: Ergebnisse für #{examn} in #{csh} wurden verändert
|
||||||
MailExamOfficeExamResultsChangedIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) verändert.
|
MailExamOfficeExamResultsChangedIntro courseName@Text termDesc@Text examn@ExamName: Ein Kursverwalter hat Prüfungsleistungen für #{examn} im Kurs #{courseName} (#{termDesc}) verändert.
|
||||||
|
|
||||||
|
MailSubjectExamOfficeExternalExamResults coursen@CourseName examn@ExamName: Ergebnisse für #{examn} in #{coursen}
|
||||||
|
MailExamOfficeExternalExamResultsIntro coursen@CourseName termDesc@Text examn@ExamName: Ein Kursverwalter hat Prüfungsleistungen für #{examn} im Kurs #{coursen} (#{termDesc}) erstellt oder angepasst.
|
||||||
|
|
||||||
MailSubjectExamRegistrationActive csh@CourseShorthand examn@ExamName: Anmeldung für #{examn} in #{csh} ist möglich
|
MailSubjectExamRegistrationActive csh@CourseShorthand examn@ExamName: Anmeldung für #{examn} in #{csh} ist möglich
|
||||||
MailExamRegistrationActiveIntro courseName@Text termDesc@Text examn@ExamName: Sie können sich nun für #{examn} im Kurs #{courseName} (#{termDesc}) anmelden.
|
MailExamRegistrationActiveIntro courseName@Text termDesc@Text examn@ExamName: Sie können sich nun für #{examn} im Kurs #{courseName} (#{termDesc}) anmelden.
|
||||||
|
|
||||||
@ -997,7 +1006,7 @@ NotificationTriggerKindCorrector: Für Korrektoren
|
|||||||
NotificationTriggerKindLecturer: Für Dozenten
|
NotificationTriggerKindLecturer: Für Dozenten
|
||||||
NotificationTriggerKindCourseLecturer: Für Kursverwalter
|
NotificationTriggerKindCourseLecturer: Für Kursverwalter
|
||||||
NotificationTriggerKindAdmin: Für Administratoren
|
NotificationTriggerKindAdmin: Für Administratoren
|
||||||
NotificationTriggerKindExamOffice: Für das Prüfungsamt
|
NotificationTriggerKindExamOffice: Für Prüfungsverwalter
|
||||||
NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
|
NotificationTriggerKindEvaluation: Für Vorlesungsumfragen
|
||||||
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
|
NotificationTriggerKindAllocationStaff: Für Zentralanmeldungen (Dozenten)
|
||||||
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
|
NotificationTriggerKindAllocationParticipant: Für Zentralanmeldungen
|
||||||
@ -1124,7 +1133,7 @@ MenuCourseMembers: Kursteilnehmer
|
|||||||
MenuCourseAddMembers: Kursteilnehmer hinzufügen
|
MenuCourseAddMembers: Kursteilnehmer hinzufügen
|
||||||
MenuCourseCommunication: Kursmitteilung (E-Mail)
|
MenuCourseCommunication: Kursmitteilung (E-Mail)
|
||||||
MenuCourseApplications: Bewerbungen
|
MenuCourseApplications: Bewerbungen
|
||||||
MenuCourseExamOffice: Prüfungsämter
|
MenuCourseExamOffice: Prüfungsbeauftragte
|
||||||
MenuTermShow: Semester
|
MenuTermShow: Semester
|
||||||
MenuSubmissionDelete: Abgabe löschen
|
MenuSubmissionDelete: Abgabe löschen
|
||||||
MenuUsers: Benutzer
|
MenuUsers: Benutzer
|
||||||
@ -1192,6 +1201,12 @@ MenuCourseNewsNew: Neue Kursnachricht
|
|||||||
MenuCourseNewsEdit: Kursnachricht bearbeiten
|
MenuCourseNewsEdit: Kursnachricht bearbeiten
|
||||||
MenuCourseEventNew: Neuer Kurstermin
|
MenuCourseEventNew: Neuer Kurstermin
|
||||||
MenuCourseEventEdit: Kurstermin bearbeiten
|
MenuCourseEventEdit: Kurstermin bearbeiten
|
||||||
|
MenuExternalExamGrades: Prüfungsleistungen
|
||||||
|
MenuExternalExamUsers: Teilnehmer
|
||||||
|
MenuExternalExamEdit: Bearbeiten
|
||||||
|
MenuExternalExamNew: Neue externe Prüfung
|
||||||
|
MenuExternalExamList: Externe Prüfungen
|
||||||
|
MenuParticipantsList: Kursteilnehmerlisten
|
||||||
|
|
||||||
BreadcrumbSubmissionFile: Datei
|
BreadcrumbSubmissionFile: Datei
|
||||||
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
BreadcrumbSubmissionUserInvite: Einladung zur Abgabe
|
||||||
@ -1245,6 +1260,20 @@ BreadcrumbExamCorrect: Eintragen von Prüfungsergebnissen
|
|||||||
BreadcrumbApplicationFiles: Bewerbungsdateien
|
BreadcrumbApplicationFiles: Bewerbungsdateien
|
||||||
BreadcrumbCourseNewsArchive: Archiv
|
BreadcrumbCourseNewsArchive: Archiv
|
||||||
BreadcrumbCourseNewsFile: Datei
|
BreadcrumbCourseNewsFile: Datei
|
||||||
|
BreadcrumbExternalExam: Externe Prüfung
|
||||||
|
BreadcrumbExternalExamList: Externe Prüfungen
|
||||||
|
BreadcrumbExternalExamNew: Neue externe Prüfung
|
||||||
|
BreadcrumbExternalExamShow coursen@CourseName examn@ExamName: #{coursen}, #{examn}
|
||||||
|
BreadcrumbExternalExamEdit: Editieren
|
||||||
|
BreadcrumbExternalExamUsers: Teilnehmer
|
||||||
|
BreadcrumbExternalExamGrades: Prüfungsleistungen
|
||||||
|
BreadcrumbExternalExamStaffInvite: Einladung zum Prüfer
|
||||||
|
BreadcrumbParticipantsList: Kursteilnehmerlisten
|
||||||
|
BreadcrumbParticipants: Kursteilnehmerliste
|
||||||
|
|
||||||
|
ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn}
|
||||||
|
ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn}
|
||||||
|
ExternalExamUsers coursen@CourseName examn@ExamName: Teilnehmer: #{coursen}, #{examn}
|
||||||
|
|
||||||
TitleMetrics: Metriken
|
TitleMetrics: Metriken
|
||||||
|
|
||||||
@ -1253,7 +1282,8 @@ AuthPredsActive: Aktive Authorisierungsprädikate
|
|||||||
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
|
AuthPredsActiveChanged: Authorisierungseinstellungen für aktuelle Sitzung gespeichert
|
||||||
AuthTagFree: Seite ist universell zugänglich
|
AuthTagFree: Seite ist universell zugänglich
|
||||||
AuthTagAdmin: Nutzer ist Administrator
|
AuthTagAdmin: Nutzer ist Administrator
|
||||||
AuthTagExamOffice: Nutzer ist Teil eines Prüfungsamts
|
AuthTagExamOffice: Nutzer ist mit Prüfungsverwaltung beauftragt
|
||||||
|
AuthTagEvaluation: Nutzer ist mit Kursumfragenverwaltung beauftragt
|
||||||
AuthTagToken: Nutzer präsentiert Authorisierungs-Token
|
AuthTagToken: Nutzer präsentiert Authorisierungs-Token
|
||||||
AuthTagNoEscalation: Nutzer-Rechte werden nicht auf fremde Institute ausgeweitet
|
AuthTagNoEscalation: Nutzer-Rechte werden nicht auf fremde Institute ausgeweitet
|
||||||
AuthTagDeprecated: Seite ist nicht überholt
|
AuthTagDeprecated: Seite ist nicht überholt
|
||||||
@ -1523,9 +1553,9 @@ ExamFinishedOffice: Noten bekannt gegeben
|
|||||||
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
|
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
|
||||||
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
|
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
|
||||||
ExamClosed: Noten gemeldet
|
ExamClosed: Noten gemeldet
|
||||||
ExamClosedTip: Prüfungsämter, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
|
ExamClosedTip: Prüfungsbeauftraget, die im System Noten einsehen, werden zu diesem Zeitpunkt benachrichtigt und danach bei Änderungen informiert
|
||||||
ExamShowGrades: Klausur ist benotet
|
ExamShowGrades: Klausur ist benotet
|
||||||
ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsämter nur informiert werden, ob die Klausur bestanden wurde?
|
ExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde?
|
||||||
ExamPublicStatistics: Statistik veröffentlichen
|
ExamPublicStatistics: Statistik veröffentlichen
|
||||||
ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können?
|
ExamPublicStatisticsTip: Soll die automatisch berechnete statistische Auswertung auch den Teilnehmern angezeigt werden, sobald diese ihre Noten einsehen können?
|
||||||
ExamAutomaticGrading: Automatische Notenberechnung
|
ExamAutomaticGrading: Automatische Notenberechnung
|
||||||
@ -1545,6 +1575,7 @@ ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
|
|||||||
ExamNoBonus': Kein automatischer Bonus
|
ExamNoBonus': Kein automatischer Bonus
|
||||||
ExamBonusPoints': Umrechnung von Übungspunkten
|
ExamBonusPoints': Umrechnung von Übungspunkten
|
||||||
ExamBonusManual': Manuelle Berechnung
|
ExamBonusManual': Manuelle Berechnung
|
||||||
|
ExamGradesExplanation: Diese Ansicht zeigt die selben Daten an, wie die Tabelle von Prüfungsteilnehmern. Anpassen der Teilnehmerdaten und Ergebnisse ist nur dort möglich. Hier können Sie vor Allem einsehen und markieren, welche Prüfungsleistungen von den zuständigen Prüfungsbeauftragten bereits vollständig bearbeitet wurden.
|
||||||
|
|
||||||
ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum
|
ExamRegisterForOccurrence: Anmeldung zur Klausur erfolgt durch Anmeldung zu einem Termin/Raum
|
||||||
|
|
||||||
@ -1674,10 +1705,16 @@ ExamUserSyncTime: Zeitpunkt
|
|||||||
ExamUserSyncSchools: Institute
|
ExamUserSyncSchools: Institute
|
||||||
ExamUserSyncLastChange: Zuletzt geändert
|
ExamUserSyncLastChange: Zuletzt geändert
|
||||||
ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
|
ExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
|
||||||
|
ExternalExamUserMarkSynchronised: Prüfungsleistung als synchronisiert markieren
|
||||||
|
ExternalExamUserMarkSynchronisedTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf.
|
||||||
|
|
||||||
ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
|
ExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
|
||||||
|
ExamUserMarkSynchronisedCsvTip: Sollen beim CSV-Export automatisch alle heruntergeladenen Prüfungsleistungen als synchronisiert markiert werden? Diese Markierung dient als Hinweis an andere Prüfungsbeauftragte und die Kursverwalter, dass die Leistung an der korrekten Stelle vermerkt wurde und keiner weiteren Handlung bedarf.
|
||||||
ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
|
ExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
|
||||||
|
|
||||||
|
ExternalExamUserMarkSynchronisedCsv: Prüfungsleistungen beim Export als synchronisiert markieren
|
||||||
|
ExternalExamUserMarkedSynchronised n@Int: #{n} #{pluralDE n "Prüfungsleistung" "Prüfungsleistungen"} als synchronisiert markiert
|
||||||
|
|
||||||
ExamOfficeExamUsersHeading: Prüfungsleistungen
|
ExamOfficeExamUsersHeading: Prüfungsleistungen
|
||||||
|
|
||||||
ActionsHead: Aktionen
|
ActionsHead: Aktionen
|
||||||
@ -1705,7 +1742,9 @@ ProportionNoRatio c@Text of@Text: #{c}/#{of}
|
|||||||
|
|
||||||
CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
|
CourseUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-teilnehmer
|
||||||
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
|
ExamUserCsvName tid@TermId ssh@SchoolId csh@CourseShorthand examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-teilnehmer
|
||||||
|
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{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
|
||||||
|
ParticipantsCsvName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-kursteilnehmer
|
||||||
|
|
||||||
CsvColumnsExplanationsLabel: Spalten- & Zellenformat
|
CsvColumnsExplanationsLabel: Spalten- & Zellenformat
|
||||||
CsvColumnsExplanationsTip: Bedeutung und Format der in der CSV-Datei enthaltenen Spalten
|
CsvColumnsExplanationsTip: Bedeutung und Format der in der CSV-Datei enthaltenen Spalten
|
||||||
@ -1780,6 +1819,11 @@ ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identi
|
|||||||
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden
|
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden
|
||||||
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
|
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
|
||||||
|
|
||||||
|
ExternalExamUserCsvRegister: Prüfungsleistung hinterlegen
|
||||||
|
ExternalExamUserCsvSetTime: Zeitpunkt anpassen
|
||||||
|
ExternalExamUserCsvSetResult: Ergebnis anpassen
|
||||||
|
ExternalExamUserCsvDeregister: Hinterlegte Prüfungsleistung löschen
|
||||||
|
|
||||||
CourseApplicationsTableCsvSetField: Bewerbungs-assoziiertes Studienfach ändern
|
CourseApplicationsTableCsvSetField: Bewerbungs-assoziiertes Studienfach ändern
|
||||||
CourseApplicationsTableCsvSetVeto: Veto setzen/entfernen
|
CourseApplicationsTableCsvSetVeto: Veto setzen/entfernen
|
||||||
CourseApplicationsTableCsvSetRating: Bewertung eintragen
|
CourseApplicationsTableCsvSetRating: Bewertung eintragen
|
||||||
@ -1837,6 +1881,11 @@ MailSchoolFunctionInviteHeading school@SchoolName renderedFunction@Text: #{rende
|
|||||||
SchoolFunctionInviteExplanation renderedFunction@Text: Sie wurden eingeladen, als #{renderedFunction} für ein Institut zu wirken. Sie erhalten, nachdem Sie die Einladung annehmen, erweiterte Rechte innerhalb des Instituts.
|
SchoolFunctionInviteExplanation renderedFunction@Text: Sie wurden eingeladen, als #{renderedFunction} für ein Institut zu wirken. Sie erhalten, nachdem Sie die Einladung annehmen, erweiterte Rechte innerhalb des Instituts.
|
||||||
SchoolFunctionInvitationAccepted school@SchoolName renderedFunction@Text: #{renderedFunction}-Einladung zum Dozent für „#{school}“ angenommen
|
SchoolFunctionInvitationAccepted school@SchoolName renderedFunction@Text: #{renderedFunction}-Einladung zum Dozent für „#{school}“ angenommen
|
||||||
|
|
||||||
|
MailSubjectExternalExamStaffInvitation coursen@CourseName examn@ExamName: Einladung zum Prüfer für „#{examn}“ in „#{coursen}“
|
||||||
|
ExternalExamStaffInviteHeading coursen@CourseName examn@ExamName: Einladung zum Prüfer für „#{examn}“ in „#{coursen}“
|
||||||
|
ExternalExamStaffInviteExplanation: Sie wurden eingeladen als Prüfer für eine Uni2work-externe Prüfung zu wirken. Sie können dann u.A. Noten für die Prüfung hinterlegen.
|
||||||
|
ExternalExamStaffInvitationAccepted coursen@CourseName examn@ExamName: Sie sind nun als Prüfer für „#{examn}“ in „#{coursen}“ eingetragen.
|
||||||
|
|
||||||
AllocationActive: Aktiv
|
AllocationActive: Aktiv
|
||||||
AllocationName: Name
|
AllocationName: Name
|
||||||
AllocationAvailableCourses: Kurse
|
AllocationAvailableCourses: Kurse
|
||||||
@ -1935,7 +1984,7 @@ SchoolExists ssh@SchoolId: Institut „#{ssh}“ existiert bereits
|
|||||||
SchoolAdmin: Admin
|
SchoolAdmin: Admin
|
||||||
SchoolLecturer: Dozent
|
SchoolLecturer: Dozent
|
||||||
SchoolEvaluation: Kursumfragenverwaltung
|
SchoolEvaluation: Kursumfragenverwaltung
|
||||||
SchoolExamOffice: Prüfungsamt
|
SchoolExamOffice: Prüfungsverwaltung
|
||||||
|
|
||||||
ApplicationEditTip: Während des Bewerbungszeitraums können eigene Bewerbungen beliebig angepasst und auch wieder zurückgezogen werden.
|
ApplicationEditTip: Während des Bewerbungszeitraums können eigene Bewerbungen beliebig angepasst und auch wieder zurückgezogen werden.
|
||||||
|
|
||||||
@ -2013,10 +2062,10 @@ MailSubjectChangeUserDisplayEmail: Diese E-Mail Adresse in Uni2work veröffentli
|
|||||||
MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer möchte „#{displayEmail}“ als öffentliche Adresse, assoziiert mit sich selbst, angeben. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte!
|
MailIntroChangeUserDisplayEmail displayEmail@UserEmail: Der oben genannte Benutzer möchte „#{displayEmail}“ als öffentliche Adresse, assoziiert mit sich selbst, angeben. Wenn Sie diese Aktion nicht selbst ausgelöst haben, ignorieren Sie diese Mitteilung bitte!
|
||||||
MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail Adresse in Uni2work veröffentlichen
|
MailTitleChangeUserDisplayEmail displayName@Text: #{displayName} möchte diese E-Mail Adresse in Uni2work veröffentlichen
|
||||||
|
|
||||||
ExamOfficeOptOutsChanged: Zuständige Prüfungsämter erfolgreich angepasst
|
ExamOfficeOptOutsChanged: Zuständige Prüfungsbeauftragte erfolgreich angepasst
|
||||||
|
|
||||||
BtnCloseExam: Klausur abschließen
|
BtnCloseExam: Klausur abschließen
|
||||||
ExamCloseTip: Wenn eine Klausur abgeschlossen wird, werden Prüfungsämter, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert.
|
ExamCloseTip: Wenn eine Klausur abgeschlossen wird, werden Prüfungsbeauftragte, die im System Noten einsehen, benachrichtigt und danach bei Änderungen informiert.
|
||||||
ExamCloseReminder: Bitte schließen Sie die Klausur frühstmöglich, sobald die Prüfungsleistungen sich voraussichtlich nicht mehr ändern werden. Z.B. direkt nach der Klausureinsicht.
|
ExamCloseReminder: Bitte schließen Sie die Klausur frühstmöglich, sobald die Prüfungsleistungen sich voraussichtlich nicht mehr ändern werden. Z.B. direkt nach der Klausureinsicht.
|
||||||
ExamDidClose: Klausur erfolgreich abgeschlossen
|
ExamDidClose: Klausur erfolgreich abgeschlossen
|
||||||
|
|
||||||
@ -2217,3 +2266,27 @@ Deficit: Defizit
|
|||||||
MetricNoSamples: Keine Messwerte
|
MetricNoSamples: Keine Messwerte
|
||||||
MetricName: Name
|
MetricName: Name
|
||||||
MetricValue: Wert
|
MetricValue: Wert
|
||||||
|
|
||||||
|
ExternalExamSemester: Semester
|
||||||
|
ExternalExamSchool: Institut
|
||||||
|
ExternalExamCourseName: Veranstaltung
|
||||||
|
ExternalExamCourseNameTip: Muss nur innerhalb von Semester und Institut eindeutig sein.
|
||||||
|
ExternalExamCourseNamePlaceholder: Analysis I, Programmierung und Modellierung, ...
|
||||||
|
ExternalExamExamName: Prüfung
|
||||||
|
ExternalExamExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein.
|
||||||
|
ExternalExamExamNamePlaceholder: Klausur, Nachklausur, Projektabnahme, ...
|
||||||
|
ExternalExamDefaultTime: Voreingestellter Zeitpunkt
|
||||||
|
ExternalExamDefaultTimePlaceholder: Zeitpunkt
|
||||||
|
ExternalExamDefaultTimeTip: Der Zeitpunkt zu dem die Prüfung abgelegt wurde, muss pro Teilnehmer festgelegt werden. Der hier angegebene Zeitpunkt wird als Standardwert für Teilnehmer verwendet, bei denen später nicht ein abweichender Zeitpunkt angegeben wird.
|
||||||
|
ExternalExamShowGrades: Klausur ist benotet
|
||||||
|
ExternalExamShowGradesTip: Sollen genaue Noten angezeigt werden, oder sollen Teilnehmer und Prüfungsbeauftragte nur informiert werden, ob die Klausur bestanden wurde?
|
||||||
|
ExternalExamExamOfficeSchools: Zusätzliche Institute
|
||||||
|
ExternalExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie hier angeben, erhalten im System (zusätzlich zum angegebenen primären Institut) volle Einsicht in sämtliche für diese Prüfung hinterlegten Leistungen, unabhängig von den Studiendaten der Teilnehmer.
|
||||||
|
ExternalExamStaff: Assoziierte Personen
|
||||||
|
ExternalExamStaffTip: Assoziierte Personen werden den Prüfungsbeauftragten und Teilnehmern angezeigt und dürfen Leistungen für die Prüfung hinterlegen.
|
||||||
|
ExternalExamStaffAlreadyAdded: Person wurde bereits der Prüfung hinzugefügt
|
||||||
|
ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, für die externen Prüfungen, die Sie anlegen
|
||||||
|
ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden.
|
||||||
|
ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits.
|
||||||
|
ExternalExamCreated coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich angelegt.
|
||||||
|
ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.
|
||||||
@ -400,7 +400,9 @@ UnauthorizedSiteAdmin: You are no system-wide administrator.
|
|||||||
UnauthorizedSchoolAdmin: You are no administrator for this department.
|
UnauthorizedSchoolAdmin: You are no administrator for this department.
|
||||||
UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator.
|
UnauthorizedAdminEscalation: You aren't an administrator for all departments for which this user is an administrator.
|
||||||
UnauthorizedExamOffice: You are not part of an exam office.
|
UnauthorizedExamOffice: You are not part of an exam office.
|
||||||
|
UnauthorizedEvaluation: You are not charged with course evaluation.
|
||||||
UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
|
UnauthorizedExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
|
||||||
|
UnauthorizedExternalExamExamOffice: You are not part of the appropriate exam office for any of the participants of this exam.
|
||||||
UnauthorizedSchoolLecturer: You are no lecturer for this department.
|
UnauthorizedSchoolLecturer: You are no lecturer for this department.
|
||||||
UnauthorizedLecturer: You are no administrator for this course.
|
UnauthorizedLecturer: You are no administrator for this course.
|
||||||
UnauthorizedAllocationLecturer: You are no administrator for any of the courses of this central allocation.
|
UnauthorizedAllocationLecturer: You are no administrator for any of the courses of this central allocation.
|
||||||
@ -412,6 +414,7 @@ UnauthorizedRegistered: You are no participant in this course.
|
|||||||
UnauthorizedAllocationRegistered: You are no participant in this central allocation.
|
UnauthorizedAllocationRegistered: You are no participant in this central allocation.
|
||||||
UnauthorizedExamResult: You have no results in this exam.
|
UnauthorizedExamResult: You have no results in this exam.
|
||||||
UnauthorizedExamOccurrenceRegistration: Registration for exam is not done including occurrence/room.
|
UnauthorizedExamOccurrenceRegistration: Registration for exam is not done including occurrence/room.
|
||||||
|
UnauthorizedExternalExamResult: You have no results in this exam.
|
||||||
UnauthorizedParticipant: The specified user is no participant of this course.
|
UnauthorizedParticipant: The specified user is no participant of this course.
|
||||||
UnauthorizedParticipantSelf: You are no participant of this course.
|
UnauthorizedParticipantSelf: You are no participant of this course.
|
||||||
UnauthorizedApplicant: The specified user is no applicant for this course.
|
UnauthorizedApplicant: The specified user is no applicant for this course.
|
||||||
@ -448,6 +451,8 @@ UnauthorizedTutor: You are no tutor.
|
|||||||
UnauthorizedTutorialRegisterGroup: You are already registered for a tutorial with the same registration group.
|
UnauthorizedTutorialRegisterGroup: You are already registered for a tutorial with the same registration group.
|
||||||
UnauthorizedLDAP: Specified user does not log in with their campus account.
|
UnauthorizedLDAP: Specified user does not log in with their campus account.
|
||||||
UnauthorizedPWHash: Specified user does not log in with an Uni2work-account.
|
UnauthorizedPWHash: Specified user does not log in with an Uni2work-account.
|
||||||
|
UnauthorizedExternalExamListNotEmpty: List of external exams is not empty
|
||||||
|
UnauthorizedExternalExamLecturer: You are not an associated person for this external exam
|
||||||
|
|
||||||
UnauthorizedPasswordResetToken: This authorisation-token may no longer be used to change passwords
|
UnauthorizedPasswordResetToken: This authorisation-token may no longer be used to change passwords
|
||||||
|
|
||||||
@ -711,6 +716,7 @@ LoginNecessary: Please log in first!
|
|||||||
|
|
||||||
InternalLdapError: Internal error during campus login
|
InternalLdapError: Internal error during campus login
|
||||||
|
|
||||||
|
CampusUserInvalidIdent: Could not determine unique identification during campus login
|
||||||
CampusUserInvalidEmail: Could not determine email address during campus login
|
CampusUserInvalidEmail: Could not determine email address during campus login
|
||||||
CampusUserInvalidDisplayName: Could not determine display name during campus login
|
CampusUserInvalidDisplayName: Could not determine display name during campus login
|
||||||
CampusUserInvalidGivenName: Could not determine given name during campus login
|
CampusUserInvalidGivenName: Could not determine given name during campus login
|
||||||
@ -862,6 +868,9 @@ MailExamOfficeExamResultsIntro courseName termDesc examn: A course administrator
|
|||||||
MailSubjectExamOfficeExamResultsChanged csh examn: Results for #{examn} of #{csh} were changed
|
MailSubjectExamOfficeExamResultsChanged csh examn: Results for #{examn} of #{csh} were changed
|
||||||
MailExamOfficeExamResultsChangedIntro courseName termDesc examn: A course administrator has changed exam results for #{examn} of the course #{courseName} (#{termDesc}).
|
MailExamOfficeExamResultsChangedIntro courseName termDesc examn: A course administrator has changed exam results for #{examn} of the course #{courseName} (#{termDesc}).
|
||||||
|
|
||||||
|
MailSubjectExamOfficeExternalExamResults coursen@CourseName examn@ExamName: Results for #{examn} in #{coursen}
|
||||||
|
MailExamOfficeExternalExamResultsIntro coursen@CourseName termDesc@Text examn@ExamName: A course administrator has changed or initially made available the results for #{examn} of the course {coursen} (#{termDesc}).
|
||||||
|
|
||||||
MailSubjectExamRegistrationActive csh examn: Registration is now allowed for #{examn} of #{csh}
|
MailSubjectExamRegistrationActive csh examn: Registration is now allowed for #{examn} of #{csh}
|
||||||
MailExamRegistrationActiveIntro courseName termDesc examn: You may now register for #{examn} of the course #{courseName} (#{termDesc}).
|
MailExamRegistrationActiveIntro courseName termDesc examn: You may now register for #{examn} of the course #{courseName} (#{termDesc}).
|
||||||
|
|
||||||
@ -1191,6 +1200,12 @@ MenuCourseNewsNew: Add course news
|
|||||||
MenuCourseNewsEdit: Edit course news
|
MenuCourseNewsEdit: Edit course news
|
||||||
MenuCourseEventNew: New course occurrence
|
MenuCourseEventNew: New course occurrence
|
||||||
MenuCourseEventEdit: Edit course occurrence
|
MenuCourseEventEdit: Edit course occurrence
|
||||||
|
MenuExternalExamGrades: Exam results
|
||||||
|
MenuExternalExamUsers: Participants
|
||||||
|
MenuExternalExamEdit: Edit
|
||||||
|
MenuExternalExamNew: New external exam
|
||||||
|
MenuExternalExamList: External exams
|
||||||
|
MenuParticipantsList: Lists of course participants
|
||||||
|
|
||||||
BreadcrumbSubmissionFile: File
|
BreadcrumbSubmissionFile: File
|
||||||
BreadcrumbSubmissionUserInvite: Invitation to participate in a submission
|
BreadcrumbSubmissionUserInvite: Invitation to participate in a submission
|
||||||
@ -1244,6 +1259,20 @@ BreadcrumbExamCorrect: Exam corrections
|
|||||||
BreadcrumbApplicationFiles: Application files
|
BreadcrumbApplicationFiles: Application files
|
||||||
BreadcrumbCourseNewsArchive: Archive
|
BreadcrumbCourseNewsArchive: Archive
|
||||||
BreadcrumbCourseNewsFile: File
|
BreadcrumbCourseNewsFile: File
|
||||||
|
BreadcrumbExternalExam: External exam
|
||||||
|
BreadcrumbExternalExamList: External exams
|
||||||
|
BreadcrumbExternalExamNew: New external exam
|
||||||
|
BreadcrumbExternalExamShow coursen@CourseName examn@ExamName: #{coursen}, #{examn}
|
||||||
|
BreadcrumbExternalExamEdit: Edit
|
||||||
|
BreadcrumbExternalExamUsers: Participants
|
||||||
|
BreadcrumbExternalExamGrades: Exam results
|
||||||
|
BreadcrumbExternalExamStaffInvite: Invitation
|
||||||
|
BreadcrumbParticipantsList: Lists of course participants
|
||||||
|
BreadcrumbParticipants: Course participants
|
||||||
|
|
||||||
|
ExternalExamEdit coursen examn: Edit: #{coursen}, #{examn}
|
||||||
|
ExternalExamGrades coursen examn: Exam achievements: #{coursen}, #{examn}
|
||||||
|
ExternalExamUsers coursen examn: Exam participants: #{coursen}, #{examn}
|
||||||
|
|
||||||
TitleMetrics: Metrics
|
TitleMetrics: Metrics
|
||||||
|
|
||||||
@ -1253,6 +1282,7 @@ AuthPredsActiveChanged: Authorisation settings saved for the current session
|
|||||||
AuthTagFree: Page is freely accessable
|
AuthTagFree: Page is freely accessable
|
||||||
AuthTagAdmin: User is administrator
|
AuthTagAdmin: User is administrator
|
||||||
AuthTagExamOffice: User is part of an exam office
|
AuthTagExamOffice: User is part of an exam office
|
||||||
|
AuthTagEvaluation: User is charged with course evaluation
|
||||||
AuthTagToken: User is presenting an authorisation-token
|
AuthTagToken: User is presenting an authorisation-token
|
||||||
AuthTagNoEscalation: User permissions are not being expanded to other departments
|
AuthTagNoEscalation: User permissions are not being expanded to other departments
|
||||||
AuthTagDeprecated: Page is not deprecated
|
AuthTagDeprecated: Page is not deprecated
|
||||||
@ -1543,6 +1573,7 @@ ExamBonusRule: Bonus points from exercises
|
|||||||
ExamNoBonus': No automatic exam bonus
|
ExamNoBonus': No automatic exam bonus
|
||||||
ExamBonusPoints': Compute from exercise achievements
|
ExamBonusPoints': Compute from exercise achievements
|
||||||
ExamBonusManual': Manual computation
|
ExamBonusManual': Manual computation
|
||||||
|
ExamGradesExplanation: This view shows the same data as the table of exam participants. Changing participant's data and achievements is only possible via the table of exam participants. Primarily, this view allows you to check and adjust which exam achievements were properly handled by the relevant exam offices.
|
||||||
|
|
||||||
ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room
|
ExamRegisterForOccurrence: Registration for this exam is done by registering for an occurrence/room
|
||||||
|
|
||||||
@ -1671,10 +1702,16 @@ ExamUserSyncTime: Timestamp
|
|||||||
ExamUserSyncSchools: Department
|
ExamUserSyncSchools: Department
|
||||||
ExamUserSyncLastChange: Last changed
|
ExamUserSyncLastChange: Last changed
|
||||||
ExamUserMarkSynchronised: Mark exam achievements as synchronised
|
ExamUserMarkSynchronised: Mark exam achievements as synchronised
|
||||||
|
ExternalExamUserMarkSynchronised: Mark exam achievements as synchronised
|
||||||
|
ExternalExamUserMarkSynchronisedTip: Should all exam achievements, that are included in the download, be marked as synchronised? Marking exam achievemnts as synchronised serves as a notice to other exam offices and course administrators, that the exam achievement has been dealt with properly such that no further action is required.
|
||||||
|
|
||||||
ExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting
|
ExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting
|
||||||
|
ExamUserMarkSynchronisedCsvTip: Should all exam achievements, that are included in the download, be marked as synchronised? Marking exam achievemnts as synchronised serves as a notice to other exam offices and course administrators, that the exam achievement has been dealt with properly such that no further action is required.
|
||||||
ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised
|
ExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised
|
||||||
|
|
||||||
|
ExternalExamUserMarkSynchronisedCsv: Mark exam achievements as synchronised while exporting
|
||||||
|
ExternalExamUserMarkedSynchronised n: Successfully marked #{n} #{pluralEN n "exam achievement" "exam achievements"} as synchronised
|
||||||
|
|
||||||
ExamOfficeExamUsersHeading: Exam achievements
|
ExamOfficeExamUsersHeading: Exam achievements
|
||||||
|
|
||||||
ActionsHead: Actions
|
ActionsHead: Actions
|
||||||
@ -1702,7 +1739,9 @@ ProportionNoRatio c of: #{c}/#{of}
|
|||||||
|
|
||||||
CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants
|
CourseUserCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-participants
|
||||||
ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants
|
ExamUserCsvName tid ssh csh examn: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase examn}-participants
|
||||||
|
ExternalExamUserCsvName tid@TermId ssh@SchoolId coursen@CourseName examn@ExamName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase coursen}-#{foldedCase examn}-participants
|
||||||
CourseApplicationsTableCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications
|
CourseApplicationsTableCsvName tid ssh csh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-applications
|
||||||
|
ParticipantsCsvName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-participants
|
||||||
|
|
||||||
CsvColumnsExplanationsLabel: Column & cell format
|
CsvColumnsExplanationsLabel: Column & cell format
|
||||||
CsvColumnsExplanationsTip: Meaning and format of the columns contained in imported and exported CSV files
|
CsvColumnsExplanationsTip: Meaning and format of the columns contained in imported and exported CSV files
|
||||||
@ -1777,6 +1816,11 @@ ExamUserCsvExceptionNoMatchingUser: Course participant could not be identified u
|
|||||||
ExamUserCsvExceptionNoMatchingStudyFeatures: The specified field did not match with any of the participant's fields of study
|
ExamUserCsvExceptionNoMatchingStudyFeatures: The specified field did not match with any of the participant's fields of study
|
||||||
ExamUserCsvExceptionNoMatchingOccurrence: Occurrence/room could not be identified uniquely
|
ExamUserCsvExceptionNoMatchingOccurrence: Occurrence/room could not be identified uniquely
|
||||||
|
|
||||||
|
ExternalExamUserCsvRegister: Store exam achievement
|
||||||
|
ExternalExamUserCsvSetTime: Adjust exam time
|
||||||
|
ExternalExamUserCsvSetResult: Adjust exam result
|
||||||
|
ExternalExamUserCsvDeregister: Delete stored exam achievement
|
||||||
|
|
||||||
CourseApplicationsTableCsvSetField: Modify field of study associated with the applicatio
|
CourseApplicationsTableCsvSetField: Modify field of study associated with the applicatio
|
||||||
CourseApplicationsTableCsvSetVeto: Set/remove veto
|
CourseApplicationsTableCsvSetVeto: Set/remove veto
|
||||||
CourseApplicationsTableCsvSetRating: Set grading
|
CourseApplicationsTableCsvSetRating: Set grading
|
||||||
@ -1834,6 +1878,11 @@ MailSchoolFunctionInviteHeading school renderedFunction: Invitation to be #{rend
|
|||||||
SchoolFunctionInviteExplanation renderedFunction: You were invited to act as #{renderedFunction} for a department. By accepting the invitation you are granted elevated rights within the department.
|
SchoolFunctionInviteExplanation renderedFunction: You were invited to act as #{renderedFunction} for a department. By accepting the invitation you are granted elevated rights within the department.
|
||||||
SchoolFunctionInvitationAccepted school renderedFunction: Successfully accepted invitation to be #{renderedFunction} for “#{school}”
|
SchoolFunctionInvitationAccepted school renderedFunction: Successfully accepted invitation to be #{renderedFunction} for “#{school}”
|
||||||
|
|
||||||
|
MailSubjectExternalExamStaffInvitation coursen examn: Invitation to act as examiner for “#{examn}” of “#{coursen}”
|
||||||
|
ExternalExamStaffInviteHeading coursen examn: Invitation to act as examiner for “#{examn}” of “#{coursen}”
|
||||||
|
ExternalExamStaffInviteExplanation: You have been invited to act as an examiner for a uni2work-external exam. After accepting you will be able to upload exam results.
|
||||||
|
ExternalExamStaffInvitationAccepted coursen examn: You are now registered as an examiner for “#{examn}” of “#{coursen}”.
|
||||||
|
|
||||||
AllocationActive: Active
|
AllocationActive: Active
|
||||||
AllocationName: Name
|
AllocationName: Name
|
||||||
AllocationAvailableCourses: Courses
|
AllocationAvailableCourses: Courses
|
||||||
@ -2215,3 +2264,27 @@ Deficit: Deficit
|
|||||||
MetricNoSamples: No samples
|
MetricNoSamples: No samples
|
||||||
MetricName: Name
|
MetricName: Name
|
||||||
MetricValue: Value
|
MetricValue: Value
|
||||||
|
|
||||||
|
ExternalExamSemester: Semester
|
||||||
|
ExternalExamSchool: Department
|
||||||
|
ExternalExamCourseName: Course
|
||||||
|
ExternalExamCourseNameTip: Needs only be unique among within semester and department.
|
||||||
|
ExternalExamCourseNamePlaceholder: Analysis I, Programming and Modelling, ...
|
||||||
|
ExternalExamExamName: Exam title
|
||||||
|
ExternalExamExamNameTip: Needs only be unique within the course.
|
||||||
|
ExternalExamExamNamePlaceholder: Exam, Exam resit, Project discussion, ...
|
||||||
|
ExternalExamDefaultTime: Default time
|
||||||
|
ExternalExamDefaultTimePlaceholder: Time
|
||||||
|
ExternalExamDefaultTimeTip: The time of the exam needs to be specified for each participant. The time entered here is used as a default value for participants for whom no different time is later specified.
|
||||||
|
ExternalExamShowGrades: Exam is graded
|
||||||
|
ExternalExamShowGradesTip: Should participants and relevant exam offices be show exact grades or only whether the exam was passed or failed?
|
||||||
|
ExternalExamExamOfficeSchools: Additional departments
|
||||||
|
ExternalExamExamOfficeSchoolsTip: Exam offices of departments you specify here will also have full access to all results for this exam disregarding the individual participants' features of study.
|
||||||
|
ExternalExamStaff: Associated persons
|
||||||
|
ExternalExamStaffTip: The list of ssociated persons is shown to exam offices and participants. Additionally associated persons may upload results for the exam.
|
||||||
|
ExternalExamStaffAlreadyAdded: Person is already associated with the exam.
|
||||||
|
ExternalExamUserMustBeStaff: You yourself must always be an associated person for exams you create.
|
||||||
|
ExternalExamCourseExists: This course already exists with uni2work. Exams for courses that exist within uni2work should be associated with the course directly instead of being created as an external exam.
|
||||||
|
ExternalExamExists coursen@CourseName examn@ExamName: Exam “#{examn}” already exists for course “#{coursen}”.
|
||||||
|
ExternalExamCreated coursen@CourseName examn@ExamName: Succesfully created exam “#{examn}” for course “#{coursen}”.
|
||||||
|
ExternalExamEdited coursen@CourseName examn@ExamName: Succesfully edited exam “#{examn}” for course “#{coursen}”.
|
||||||
|
|||||||
52
missing-translations.sh
Executable file
52
missing-translations.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function translations() {
|
||||||
|
msgFile=$1
|
||||||
|
|
||||||
|
sed -r 's/^([^ :]+).*$/\1/' ${msgFile} \
|
||||||
|
| sort
|
||||||
|
}
|
||||||
|
|
||||||
|
typeset -a msgFiles
|
||||||
|
msgFiles=(messages/**/*.msg)
|
||||||
|
|
||||||
|
typeset -a msgDirectories
|
||||||
|
msgDirectories=()
|
||||||
|
for msgFile (${msgFiles}); do
|
||||||
|
if ! [[ ${msgDirectories[(ie)${msgFile:h}]} -le ${#msgDirectories} ]]; then
|
||||||
|
msgDirectories+=(${msgFile:h})
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for msgDirectory (${msgDirectories}); do
|
||||||
|
typeset -a dirMsgFiles
|
||||||
|
dirMsgFiles=()
|
||||||
|
for msgFile (${msgFiles}); do
|
||||||
|
if [[ ${msgFile:h} == ${msgDirectory} ]]; then
|
||||||
|
dirMsgFiles+=(${msgFile})
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
(
|
||||||
|
diffDir=""
|
||||||
|
function cleanup() {
|
||||||
|
cd
|
||||||
|
[[ -n ${diffDir} ]] && rm -rf ${diffDir}
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
diffDir=$(mktemp -d)
|
||||||
|
|
||||||
|
typeset -a diffArgs
|
||||||
|
diffArgs=()
|
||||||
|
|
||||||
|
for msgFile (${dirMsgFiles}); do
|
||||||
|
translations ${msgFile} > ${diffDir}/${msgFile:t}
|
||||||
|
diffArgs+=(${diffDir}/${msgFile:t})
|
||||||
|
done
|
||||||
|
|
||||||
|
printf ">>> %s\n" ${msgDirectory}
|
||||||
|
diff --suppress-common-lines -wB ${diffArgs}
|
||||||
|
)
|
||||||
|
done
|
||||||
@ -11,4 +11,9 @@ ExamOfficeResultSynced
|
|||||||
school SchoolId Maybe
|
school SchoolId Maybe
|
||||||
office UserId
|
office UserId
|
||||||
result ExamResultId
|
result ExamResultId
|
||||||
|
time UTCTime
|
||||||
|
ExamOfficeExternalResultSynced
|
||||||
|
school SchoolId Maybe
|
||||||
|
office UserId
|
||||||
|
result ExternalExamResultId
|
||||||
time UTCTime
|
time UTCTime
|
||||||
23
models/external-exams.model
Normal file
23
models/external-exams.model
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
ExternalExam
|
||||||
|
term TermId
|
||||||
|
school SchoolId
|
||||||
|
courseName (CI Text)
|
||||||
|
examName (CI Text)
|
||||||
|
defaultTime UTCTime Maybe
|
||||||
|
showGrades Bool
|
||||||
|
UniqueExternalExam term school courseName examName
|
||||||
|
ExternalExamResult
|
||||||
|
user UserId
|
||||||
|
exam ExternalExamId
|
||||||
|
result ExamResultGrade
|
||||||
|
time UTCTime
|
||||||
|
lastChanged UTCTime
|
||||||
|
UniqueExternalExamResult exam user
|
||||||
|
ExternalExamStaff
|
||||||
|
user UserId
|
||||||
|
exam ExternalExamId
|
||||||
|
UniqueExternalExamStaff exam user
|
||||||
|
ExternalExamOfficeSchool
|
||||||
|
school SchoolId
|
||||||
|
exam ExternalExamId
|
||||||
|
UniqueExternalExamOfficeSchool exam school
|
||||||
239
package-lock.json
generated
239
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "10.3.0",
|
"version": "10.4.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4752,6 +4752,18 @@
|
|||||||
"yargs": "12.0.1"
|
"yargs": "12.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||||
|
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"co": "^4.6.0",
|
||||||
|
"fast-deep-equal": "^1.0.0",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"date-format": {
|
"date-format": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
|
||||||
@ -4767,6 +4779,28 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"har-validator": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||||
|
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^5.1.0",
|
||||||
|
"har-schema": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||||
|
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"log4js": {
|
"log4js": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.3.tgz",
|
||||||
@ -4785,6 +4819,52 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"oauth-sign": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||||
|
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"punycode": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||||
|
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"version": "2.87.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
|
||||||
|
"integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"aws-sign2": "~0.7.0",
|
||||||
|
"aws4": "^1.6.0",
|
||||||
|
"caseless": "~0.12.0",
|
||||||
|
"combined-stream": "~1.0.5",
|
||||||
|
"extend": "~3.0.1",
|
||||||
|
"forever-agent": "~0.6.1",
|
||||||
|
"form-data": "~2.3.1",
|
||||||
|
"har-validator": "~5.0.3",
|
||||||
|
"http-signature": "~1.2.0",
|
||||||
|
"is-typedarray": "~1.0.0",
|
||||||
|
"isstream": "~0.1.2",
|
||||||
|
"json-stringify-safe": "~5.0.1",
|
||||||
|
"mime-types": "~2.1.17",
|
||||||
|
"oauth-sign": "~0.8.2",
|
||||||
|
"performance-now": "^2.1.0",
|
||||||
|
"qs": "~6.5.1",
|
||||||
|
"safe-buffer": "^5.1.1",
|
||||||
|
"tough-cookie": "~2.3.3",
|
||||||
|
"tunnel-agent": "^0.6.0",
|
||||||
|
"uuid": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"streamroller": {
|
"streamroller": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
|
||||||
@ -4797,6 +4877,15 @@
|
|||||||
"readable-stream": "^2.3.0"
|
"readable-stream": "^2.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tough-cookie": {
|
||||||
|
"version": "2.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
||||||
|
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "^1.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-6.0.0.tgz",
|
||||||
@ -8626,14 +8715,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs-extra": {
|
"fs-extra": {
|
||||||
"version": "7.0.1",
|
"version": "8.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"graceful-fs": "^4.1.2",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^4.0.0",
|
"jsonfile": "^4.0.0",
|
||||||
"universalify": "^0.1.0"
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs-minipass": {
|
"fs-minipass": {
|
||||||
@ -9836,39 +9933,13 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"har-validator": {
|
"har-validator": {
|
||||||
"version": "5.0.3",
|
"version": "5.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^5.1.0",
|
"ajv": "^6.5.5",
|
||||||
"har-schema": "^2.0.0"
|
"har-schema": "^2.0.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ajv": {
|
|
||||||
"version": "5.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
|
||||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"co": "^4.6.0",
|
|
||||||
"fast-deep-equal": "^1.0.0",
|
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
|
||||||
"json-schema-traverse": "^0.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fast-deep-equal": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
|
||||||
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"json-schema-traverse": {
|
|
||||||
"version": "0.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
|
||||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"has": {
|
"has": {
|
||||||
@ -15639,9 +15710,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"oauth-sign": {
|
"oauth-sign": {
|
||||||
"version": "0.8.2",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||||
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
|
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
@ -17332,6 +17403,12 @@
|
|||||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"psl": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"public-encrypt": {
|
"public-encrypt": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
|
||||||
@ -17802,31 +17879,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"version": "2.87.0",
|
"version": "2.88.0",
|
||||||
"resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||||
"integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==",
|
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"aws-sign2": "~0.7.0",
|
"aws-sign2": "~0.7.0",
|
||||||
"aws4": "^1.6.0",
|
"aws4": "^1.8.0",
|
||||||
"caseless": "~0.12.0",
|
"caseless": "~0.12.0",
|
||||||
"combined-stream": "~1.0.5",
|
"combined-stream": "~1.0.6",
|
||||||
"extend": "~3.0.1",
|
"extend": "~3.0.2",
|
||||||
"forever-agent": "~0.6.1",
|
"forever-agent": "~0.6.1",
|
||||||
"form-data": "~2.3.1",
|
"form-data": "~2.3.2",
|
||||||
"har-validator": "~5.0.3",
|
"har-validator": "~5.1.0",
|
||||||
"http-signature": "~1.2.0",
|
"http-signature": "~1.2.0",
|
||||||
"is-typedarray": "~1.0.0",
|
"is-typedarray": "~1.0.0",
|
||||||
"isstream": "~0.1.2",
|
"isstream": "~0.1.2",
|
||||||
"json-stringify-safe": "~5.0.1",
|
"json-stringify-safe": "~5.0.1",
|
||||||
"mime-types": "~2.1.17",
|
"mime-types": "~2.1.19",
|
||||||
"oauth-sign": "~0.8.2",
|
"oauth-sign": "~0.9.0",
|
||||||
"performance-now": "^2.1.0",
|
"performance-now": "^2.1.0",
|
||||||
"qs": "~6.5.1",
|
"qs": "~6.5.2",
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.2",
|
||||||
"tough-cookie": "~2.3.3",
|
"tough-cookie": "~2.4.3",
|
||||||
"tunnel-agent": "^0.6.0",
|
"tunnel-agent": "^0.6.0",
|
||||||
"uuid": "^3.1.0"
|
"uuid": "^3.3.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"qs": {
|
"qs": {
|
||||||
@ -17837,6 +17914,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"request-promise": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "^3.5.0",
|
||||||
|
"request-promise-core": "1.1.3",
|
||||||
|
"stealthy-require": "^1.1.1",
|
||||||
|
"tough-cookie": "^2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"request-promise-core": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"require-directory": {
|
"require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@ -19098,6 +19196,12 @@
|
|||||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"stealthy-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"stream-browserify": {
|
"stream-browserify": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
||||||
@ -19159,6 +19263,17 @@
|
|||||||
"ms": "^2.1.1"
|
"ms": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fs-extra": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@ -19752,11 +19867,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.3.4",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||||
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
|
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"psl": "^1.1.24",
|
||||||
"punycode": "^1.4.1"
|
"punycode": "^1.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -20550,6 +20666,19 @@
|
|||||||
"lodash": ">=3.5 <5",
|
"lodash": ">=3.5 <5",
|
||||||
"object.entries": "^1.1.0",
|
"object.entries": "^1.1.0",
|
||||||
"tapable": "^1.0.0"
|
"tapable": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fs-extra": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-plugin-hash-output": {
|
"webpack-plugin-hash-output": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "10.3.0",
|
"version": "10.4.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -74,6 +74,7 @@
|
|||||||
"css-loader": "^2.1.1",
|
"css-loader": "^2.1.1",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"file-loader": "^5.0.2",
|
"file-loader": "^5.0.2",
|
||||||
|
"fs-extra": "^8.1.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"husky": "^2.7.0",
|
"husky": "^2.7.0",
|
||||||
@ -96,6 +97,8 @@
|
|||||||
"postcss-preset-env": "^6.7.0",
|
"postcss-preset-env": "^6.7.0",
|
||||||
"real-favicon-webpack-plugin": "^0.2.3",
|
"real-favicon-webpack-plugin": "^0.2.3",
|
||||||
"remove-files-webpack-plugin": "^1.1.3",
|
"remove-files-webpack-plugin": "^1.1.3",
|
||||||
|
"request": "^2.88.0",
|
||||||
|
"request-promise": "^4.2.5",
|
||||||
"resolve-url-loader": "^3.1.1",
|
"resolve-url-loader": "^3.1.1",
|
||||||
"sass": "^1.23.7",
|
"sass": "^1.23.7",
|
||||||
"sass-loader": "^7.3.1",
|
"sass-loader": "^7.3.1",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: uniworx
|
name: uniworx
|
||||||
version: 10.3.0
|
version: 10.4.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- base
|
- base
|
||||||
|
|||||||
13
routes
13
routes
@ -80,6 +80,16 @@
|
|||||||
/users EOUsersR GET POST
|
/users EOUsersR GET POST
|
||||||
/users/invite EOUsersInviteR GET POST
|
/users/invite EOUsersInviteR GET POST
|
||||||
|
|
||||||
|
/external-exam EExamListR GET !lecturer !¬empty
|
||||||
|
/external-exam/new EExamNewR GET POST !lecturer
|
||||||
|
/external-exam/#TermId/#SchoolId/#CourseName/#ExamName EExamR !lecturer:
|
||||||
|
/ EEShowR GET !exam-office !exam-result
|
||||||
|
/edit EEEditR GET POST
|
||||||
|
/users EEUsersR GET POST
|
||||||
|
/grades EEGradesR GET POST !exam-office
|
||||||
|
/staff-invite EEStaffInviteR GET POST
|
||||||
|
|
||||||
|
|
||||||
/term TermShowR GET !free
|
/term TermShowR GET !free
|
||||||
/term/current TermCurrentR GET !free
|
/term/current TermCurrentR GET !free
|
||||||
/term/edit TermEditR GET POST
|
/term/edit TermEditR GET POST
|
||||||
@ -98,6 +108,9 @@
|
|||||||
/register ARegisterR POST !time
|
/register ARegisterR POST !time
|
||||||
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
|
/course/#CryptoUUIDCourse/apply AApplyR POST !timeANDallocation-registered
|
||||||
|
|
||||||
|
/participants ParticipantsListR GET !evaluation
|
||||||
|
/participants/#TermId/#SchoolId ParticipantsR GET !evaluation
|
||||||
|
|
||||||
|
|
||||||
-- For Pattern Synonyms see Foundation
|
-- For Pattern Synonyms see Foundation
|
||||||
/course/ CourseListR GET !free
|
/course/ CourseListR GET !free
|
||||||
|
|||||||
@ -19,7 +19,7 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
override = oldAttrs: {
|
override = oldAttrs: {
|
||||||
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-12_x postgresql openldap google-chrome exiftool ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install weeder ]);
|
nativeBuildInputs = oldAttrs.nativeBuildInputs ++ (with pkgs; [ nodejs-13_x postgresql openldap google-chrome exiftool ]) ++ (with haskellPackages; [ stack yesod-bin hlint cabal-install weeder ]);
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
export PROMPT_INFO="${oldAttrs.name}"
|
export PROMPT_INFO="${oldAttrs.name}"
|
||||||
|
|
||||||
|
|||||||
@ -122,6 +122,8 @@ import Handler.Exam
|
|||||||
import Handler.Allocation
|
import Handler.Allocation
|
||||||
import Handler.ExamOffice
|
import Handler.ExamOffice
|
||||||
import Handler.Metrics
|
import Handler.Metrics
|
||||||
|
import Handler.ExternalExam
|
||||||
|
import Handler.Participants
|
||||||
|
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
|
|||||||
@ -139,6 +139,45 @@ data Transaction
|
|||||||
{ transactionTutorial :: TutorialId
|
{ transactionTutorial :: TutorialId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
| TransactionExternalExamEdit
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
}
|
||||||
|
|
||||||
|
| TransactionExternalExamOfficeSchoolEdit
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionSchool :: SchoolId
|
||||||
|
}
|
||||||
|
| TransactionExternalExamOfficeSchoolDelete
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionSchool :: SchoolId
|
||||||
|
}
|
||||||
|
|
||||||
|
| TransactionExternalExamStaffEdit
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionUser :: UserId
|
||||||
|
}
|
||||||
|
| TransactionExternalExamStaffDelete
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionUser :: UserId
|
||||||
|
}
|
||||||
|
| TransactionExternalExamStaffInviteEdit
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionEmail :: UserEmail
|
||||||
|
}
|
||||||
|
| TransactionExternalExamStaffInviteDelete
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionEmail :: UserEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
| TransactionExternalExamResultEdit
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionUser :: UserId
|
||||||
|
}
|
||||||
|
| TransactionExternalExamResultDelete
|
||||||
|
{ transactionExternalExam :: ExternalExamId
|
||||||
|
, transactionUser :: UserId
|
||||||
|
}
|
||||||
|
|
||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
|
|||||||
@ -3,6 +3,7 @@ module Auth.LDAP
|
|||||||
, campusLogin
|
, campusLogin
|
||||||
, CampusUserException(..)
|
, CampusUserException(..)
|
||||||
, campusUser, campusUser'
|
, campusUser, campusUser'
|
||||||
|
, campusUserMatr, campusUserMatr'
|
||||||
, CampusMessage(..)
|
, CampusMessage(..)
|
||||||
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
, ldapUserPrincipalName, ldapUserEmail, ldapUserDisplayName
|
||||||
, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname
|
, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname
|
||||||
@ -43,7 +44,7 @@ data CampusMessage = MsgCampusIdentPlaceholder
|
|||||||
|
|
||||||
|
|
||||||
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
findUser :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
||||||
findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase userSearchSettings) retAttrs) userFilters
|
findUser conf@LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
||||||
where
|
where
|
||||||
userFilters =
|
userFilters =
|
||||||
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserPrincipalName Ldap.:= Text.encodeUtf8 ident
|
||||||
@ -54,14 +55,24 @@ findUser LdapConf{..} ldap ident retAttrs = fromMaybe [] <$> findM (assertM (not
|
|||||||
, ldapUserEmail' <- toList ldapUserEmail
|
, ldapUserEmail' <- toList ldapUserEmail
|
||||||
] ++
|
] ++
|
||||||
[ ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident
|
[ ldapUserDisplayName Ldap.:= Text.encodeUtf8 ident
|
||||||
|
, ldapUserMatriculation Ldap.:= Text.encodeUtf8 ident
|
||||||
]
|
]
|
||||||
userSearchSettings = mconcat
|
|
||||||
[ Ldap.scope ldapScope
|
findUserMatr :: LdapConf -> Ldap -> Text -> [Ldap.Attr] -> IO [Ldap.SearchEntry]
|
||||||
, Ldap.size 2
|
findUserMatr conf@LdapConf{..} ldap userMatr retAttrs = fromMaybe [] <$> findM (assertM (not . null) . lift . flip (Ldap.search ldap ldapBase $ userSearchSettings conf) retAttrs) userFilters
|
||||||
, Ldap.time ldapSearchTimeout
|
where
|
||||||
, Ldap.derefAliases Ldap.DerefAlways
|
userFilters =
|
||||||
|
[ ldapUserMatriculation Ldap.:= Text.encodeUtf8 userMatr
|
||||||
]
|
]
|
||||||
|
|
||||||
|
userSearchSettings :: LdapConf -> Ldap.Mod Ldap.Search
|
||||||
|
userSearchSettings LdapConf{..} = mconcat
|
||||||
|
[ Ldap.scope ldapScope
|
||||||
|
, Ldap.size 2
|
||||||
|
, Ldap.time ldapSearchTimeout
|
||||||
|
, Ldap.derefAliases Ldap.DerefAlways
|
||||||
|
]
|
||||||
|
|
||||||
ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr
|
ldapUserPrincipalName, ldapUserDisplayName, ldapUserMatriculation, ldapUserFirstName, ldapUserSurname, ldapUserTitle, ldapUserStudyFeatures, ldapUserFieldName, ldapUserSchoolAssociation, ldapSex, ldapUserSubTermsSemester :: Ldap.Attr
|
||||||
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
ldapUserPrincipalName = Ldap.Attr "userPrincipalName"
|
||||||
ldapUserDisplayName = Ldap.Attr "displayName"
|
ldapUserDisplayName = Ldap.Attr "displayName"
|
||||||
@ -99,13 +110,7 @@ campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $
|
|||||||
results <- case lookup "DN" credsExtra of
|
results <- case lookup "DN" credsExtra of
|
||||||
Just userDN -> do
|
Just userDN -> do
|
||||||
let userFilter = Ldap.Present ldapUserPrincipalName
|
let userFilter = Ldap.Present ldapUserPrincipalName
|
||||||
userSearchSettings = mconcat
|
Ldap.search ldap (Ldap.Dn userDN) (userSearchSettings conf) userFilter []
|
||||||
[ Ldap.scope Ldap.BaseObject
|
|
||||||
, Ldap.size 2
|
|
||||||
, Ldap.time ldapSearchTimeout
|
|
||||||
, Ldap.derefAliases Ldap.DerefAlways
|
|
||||||
]
|
|
||||||
Ldap.search ldap (Ldap.Dn userDN) userSearchSettings userFilter []
|
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
findUser conf ldap credsIdent []
|
findUser conf ldap credsIdent []
|
||||||
case results of
|
case results of
|
||||||
@ -123,6 +128,26 @@ campusUser' conf pool User{userIdent}
|
|||||||
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) [])
|
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) $ campusUser conf pool (Creds apLdap (CI.original userIdent) [])
|
||||||
|
|
||||||
|
|
||||||
|
campusUserMatr :: MonadUnliftIO m => LdapConf -> LdapPool -> UserMatriculation -> m (Ldap.AttrList [])
|
||||||
|
campusUserMatr conf@LdapConf{..} pool userMatr = liftIO . (`catches` errHandlers) $ either (throwM . CampusUserLdapError) return <=< withLdap pool $ \ldap -> do
|
||||||
|
Ldap.bind ldap ldapDn ldapPassword
|
||||||
|
results <- findUserMatr conf ldap userMatr []
|
||||||
|
case results of
|
||||||
|
[] -> throwM CampusUserNoResult
|
||||||
|
[Ldap.SearchEntry _ attrs] -> return attrs
|
||||||
|
_otherwise -> throwM CampusUserAmbiguous
|
||||||
|
where
|
||||||
|
errHandlers = [ Exc.Handler $ \LineTooLong -> throwM CampusUserLineTooLong
|
||||||
|
, Exc.Handler $ \(HostNotResolved host) -> throwM $ CampusUserHostNotResolved host
|
||||||
|
, Exc.Handler $ \(HostCannotConnect host excs) -> throwM $ CampusUserHostCannotConnect host excs
|
||||||
|
]
|
||||||
|
|
||||||
|
campusUserMatr' :: (MonadCatch m, MonadUnliftIO m) => LdapConf -> LdapPool -> UserMatriculation -> m (Maybe (Ldap.AttrList []))
|
||||||
|
campusUserMatr' conf pool
|
||||||
|
= runMaybeT . catchIfMaybeT (is _CampusUserNoResult) . campusUserMatr conf pool
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
campusForm :: ( RenderMessage (HandlerSite m) FormMessage
|
campusForm :: ( RenderMessage (HandlerSite m) FormMessage
|
||||||
, RenderMessage (HandlerSite m) CampusMessage
|
, RenderMessage (HandlerSite m) CampusMessage
|
||||||
, MonadHandler m
|
, MonadHandler m
|
||||||
|
|||||||
@ -17,7 +17,7 @@ module Database.Esqueleto.Utils
|
|||||||
, selectExists
|
, selectExists
|
||||||
, SqlHashable
|
, SqlHashable
|
||||||
, sha256
|
, sha256
|
||||||
, maybe
|
, maybe, unsafeCoalesce
|
||||||
, SqlProject(..)
|
, SqlProject(..)
|
||||||
, (->.)
|
, (->.)
|
||||||
, fromSqlKey
|
, fromSqlKey
|
||||||
@ -236,6 +236,9 @@ maybe onNothing onJust val = E.case_
|
|||||||
]
|
]
|
||||||
(E.else_ onNothing)
|
(E.else_ onNothing)
|
||||||
|
|
||||||
|
unsafeCoalesce :: E.PersistField a => [E.SqlExpr (E.Value (Maybe a))] -> E.SqlExpr (E.Value a)
|
||||||
|
unsafeCoalesce = E.veryUnsafeCoerceSqlExprValue . E.coalesce
|
||||||
|
|
||||||
|
|
||||||
class (PersistEntity entity, PersistField value) => SqlProject entity value entity' value' | entity value entity' -> value', entity value value' -> entity' where
|
class (PersistEntity entity, PersistField value) => SqlProject entity value entity' value' | entity value entity' -> value', entity value value' -> entity' where
|
||||||
sqlProject :: E.SqlExpr entity' -> EntityField entity value -> E.SqlExpr (E.Value value')
|
sqlProject :: E.SqlExpr entity' -> EntityField entity value -> E.SqlExpr (E.Value value')
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import Auth.Dummy
|
|||||||
import qualified Network.Wai as W (pathInfo)
|
import qualified Network.Wai as W (pathInfo)
|
||||||
|
|
||||||
import qualified Yesod.Core.Unsafe as Unsafe
|
import qualified Yesod.Core.Unsafe as Unsafe
|
||||||
import Data.CaseInsensitive (original, mk)
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
import Data.ByteArray (convert)
|
import Data.ByteArray (convert)
|
||||||
import Crypto.Hash (Digest, SHAKE256, SHAKE128)
|
import Crypto.Hash (Digest, SHAKE256, SHAKE128)
|
||||||
@ -45,6 +45,7 @@ import qualified Data.Set as Set
|
|||||||
import Data.Map (Map, (!?))
|
import Data.Map (Map, (!?))
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.HashSet as HashSet
|
import qualified Data.HashSet as HashSet
|
||||||
|
import qualified Data.List.NonEmpty as NonEmpty
|
||||||
|
|
||||||
import Data.List (nubBy, (!!), findIndex, inits)
|
import Data.List (nubBy, (!!), findIndex, inits)
|
||||||
import qualified Data.List as List
|
import qualified Data.List as List
|
||||||
@ -69,6 +70,7 @@ import qualified Control.Monad.Catch as C
|
|||||||
import Handler.Utils.StudyFeatures
|
import Handler.Utils.StudyFeatures
|
||||||
import Handler.Utils.SchoolLdap
|
import Handler.Utils.SchoolLdap
|
||||||
import Handler.Utils.ExamOffice.Exam
|
import Handler.Utils.ExamOffice.Exam
|
||||||
|
import Handler.Utils.ExamOffice.ExternalExam
|
||||||
import Handler.Utils.ExamOffice.Course
|
import Handler.Utils.ExamOffice.Course
|
||||||
import Handler.Utils.Profile
|
import Handler.Utils.Profile
|
||||||
import Handler.Utils.Routes
|
import Handler.Utils.Routes
|
||||||
@ -371,10 +373,34 @@ tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of
|
|||||||
E.where_ $ examOfficeExamResultAuth (E.val authId) examResult
|
E.where_ $ examOfficeExamResultAuth (E.val authId) examResult
|
||||||
guardMExceptT hasUsers (unauthorizedI MsgUnauthorizedExamExamOffice)
|
guardMExceptT hasUsers (unauthorizedI MsgUnauthorizedExamExamOffice)
|
||||||
return Authorized
|
return Authorized
|
||||||
|
EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
hasUsers <- lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamResult) -> do
|
||||||
|
E.on $ eexam E.^. ExternalExamId E.==. eexamResult E.^. ExternalExamResultExam
|
||||||
|
|
||||||
|
E.where_ $ eexam E.^. ExternalExamTerm E.==. E.val tid
|
||||||
|
E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
|
||||||
|
E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
|
||||||
|
E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn
|
||||||
|
|
||||||
|
E.where_ $ examOfficeExternalExamResultAuth (E.val authId) eexamResult
|
||||||
|
guardMExceptT hasUsers $ unauthorizedI MsgUnauthorizedExternalExamExamOffice
|
||||||
|
return Authorized
|
||||||
_other -> $cachedHereBinary mAuthId . exceptT return return $ do
|
_other -> $cachedHereBinary mAuthId . exceptT return return $ do
|
||||||
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice] []
|
isExamOffice <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice]
|
||||||
guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedExamOffice)
|
guardMExceptT isExamOffice (unauthorizedI MsgUnauthorizedExamOffice)
|
||||||
|
return Authorized
|
||||||
|
tagAccessPredicate AuthEvaluation = APDB $ \mAuthId route _ -> case route of
|
||||||
|
ParticipantsR tid ssh -> $cachedHereBinary (mAuthId, tid, ssh) . exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolEvaluation
|
||||||
|
guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
|
||||||
|
return Authorized
|
||||||
|
_other -> $cachedHereBinary mAuthId . exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolEvaluation]
|
||||||
|
guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
|
||||||
return Authorized
|
return Authorized
|
||||||
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $
|
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $
|
||||||
lift . validateToken mAuthId route isWrite =<< askTokenUnsafe
|
lift . validateToken mAuthId route isWrite =<< askTokenUnsafe
|
||||||
@ -420,7 +446,18 @@ tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of
|
|||||||
E.&&. allocation E.^. AllocationTerm E.==. E.val tid
|
E.&&. allocation E.^. AllocationTerm E.==. E.val tid
|
||||||
E.&&. allocation E.^. AllocationSchool E.==. E.val ssh
|
E.&&. allocation E.^. AllocationSchool E.==. E.val ssh
|
||||||
E.&&. allocation E.^. AllocationShorthand E.==. E.val ash
|
E.&&. allocation E.^. AllocationShorthand E.==. E.val ash
|
||||||
guardMExceptT isLecturer (unauthorizedI MsgUnauthorizedAllocationLecturer)
|
guardMExceptT isLecturer $ unauthorizedI MsgUnauthorizedAllocationLecturer
|
||||||
|
return Authorized
|
||||||
|
EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
isLecturer <- lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` staff) -> do
|
||||||
|
E.on $ eexam E.^. ExternalExamId E.==. staff E.^. ExternalExamStaffExam
|
||||||
|
E.where_ $ staff E.^. ExternalExamStaffUser E.==. E.val authId
|
||||||
|
E.&&. eexam E.^. ExternalExamTerm E.==. E.val tid
|
||||||
|
E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
|
||||||
|
E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
|
||||||
|
E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn
|
||||||
|
guardMExceptT isLecturer $ unauthorizedI MsgUnauthorizedExternalExamLecturer
|
||||||
return Authorized
|
return Authorized
|
||||||
-- lecturer for any school will do
|
-- lecturer for any school will do
|
||||||
_ -> $cachedHereBinary mAuthId . exceptT return return $ do
|
_ -> $cachedHereBinary mAuthId . exceptT return return $ do
|
||||||
@ -886,6 +923,17 @@ tagAccessPredicate AuthExamResult = APDB $ \mAuthId route _ -> case route of
|
|||||||
E.&&. exam E.^. ExamName E.==. E.val examn
|
E.&&. exam E.^. ExamName E.==. E.val examn
|
||||||
guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult)
|
guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult)
|
||||||
return Authorized
|
return Authorized
|
||||||
|
EExamR tid ssh coursen examn _ -> $cachedHereBinary (mAuthId, tid, ssh, coursen, examn) . exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
hasResult <- $cachedHereBinary (authId, tid, ssh, coursen, examn) . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamResult) -> do
|
||||||
|
E.on $ eexam E.^. ExternalExamId E.==. eexamResult E.^. ExternalExamResultExam
|
||||||
|
E.where_ $ eexamResult E.^. ExternalExamResultUser E.==. E.val authId
|
||||||
|
E.&&. eexam E.^. ExternalExamTerm E.==. E.val tid
|
||||||
|
E.&&. eexam E.^. ExternalExamSchool E.==. E.val ssh
|
||||||
|
E.&&. eexam E.^. ExternalExamCourseName E.==. E.val coursen
|
||||||
|
E.&&. eexam E.^. ExternalExamExamName E.==. E.val examn
|
||||||
|
guardMExceptT hasResult $ unauthorizedI MsgUnauthorizedExternalExamResult
|
||||||
|
return Authorized
|
||||||
CourseR tid ssh csh _ -> exceptT return return $ do
|
CourseR tid ssh csh _ -> exceptT return return $ do
|
||||||
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
hasResult <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
|
hasResult <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
|
||||||
@ -1049,7 +1097,14 @@ tagAccessPredicate AuthRegisterGroup = APDB $ \mAuthId route _ -> case route of
|
|||||||
guard $ not hasOther
|
guard $ not hasOther
|
||||||
return Authorized
|
return Authorized
|
||||||
r -> $unsupportedAuthPredicate AuthRegisterGroup r
|
r -> $unsupportedAuthPredicate AuthRegisterGroup r
|
||||||
tagAccessPredicate AuthEmpty = APDB $ \_ route _ -> case route of
|
tagAccessPredicate AuthEmpty = APDB $ \mAuthId route _ -> case route of
|
||||||
|
EExamListR -> exceptT return return $ do
|
||||||
|
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
|
||||||
|
hasExternalExams <- $cachedHereBinary authId . lift . E.selectExists . E.from $ \(eexam `E.InnerJoin` eexamStaff) -> do
|
||||||
|
E.on $ eexam E.^. ExternalExamId E.==. eexamStaff E.^. ExternalExamStaffExam
|
||||||
|
E.where_ $ eexamStaff E.^. ExternalExamStaffUser E.==. E.val authId
|
||||||
|
guardMExceptT (not hasExternalExams) $ unauthorizedI MsgUnauthorizedExternalExamListNotEmpty
|
||||||
|
return Authorized
|
||||||
CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNotEmpty) $ do
|
CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgCourseNotEmpty) $ do
|
||||||
-- Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
|
-- Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
|
||||||
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
@ -1516,8 +1571,9 @@ siteLayout' headingOverride widget = do
|
|||||||
where
|
where
|
||||||
go crumbs Nothing = return crumbs
|
go crumbs Nothing = return crumbs
|
||||||
go crumbs (Just cRoute) = do
|
go crumbs (Just cRoute) = do
|
||||||
|
hasAccess <- hasReadAccessTo cRoute
|
||||||
(title, next) <- breadcrumb cRoute
|
(title, next) <- breadcrumb cRoute
|
||||||
go ((cRoute, title) : crumbs) next
|
go ((cRoute, title, hasAccess) : crumbs) next
|
||||||
(title, parents) <- breadcrumbs' mcurrentRoute
|
(title, parents) <- breadcrumbs' mcurrentRoute
|
||||||
|
|
||||||
-- let isParent :: Route UniWorX -> Bool
|
-- let isParent :: Route UniWorX -> Bool
|
||||||
@ -1604,7 +1660,7 @@ siteLayout' headingOverride widget = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority
|
let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority
|
||||||
highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents
|
highlight = let crumbs = mcons mcurrentRoute $ view _1 <$> reverse parents
|
||||||
navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes
|
navItems = map (view _2) favourites ++ map (urlRoute . menuItemRoute . view _1) menuTypes
|
||||||
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
|
highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map (view _2) favourites) crumbs
|
||||||
in \r -> Just r == highR
|
in \r -> Just r == highR
|
||||||
@ -1753,7 +1809,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR
|
breadcrumb SchoolListR = i18nCrumb MsgMenuSchoolList $ Just AdminR
|
||||||
breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do
|
breadcrumb (SchoolR ssh SchoolEditR) = maybeT (i18nCrumb MsgBreadcrumbSchool $ Just SchoolListR) $ do
|
||||||
School{..} <- MaybeT . runDB $ get ssh
|
School{..} <- MaybeT . runDB $ get ssh
|
||||||
return (original schoolName, Just SchoolListR)
|
return (CI.original schoolName, Just SchoolListR)
|
||||||
breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR
|
breadcrumb SchoolNewR = i18nCrumb MsgMenuSchoolNew $ Just SchoolListR
|
||||||
|
|
||||||
breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing
|
breadcrumb (ExamOfficeR EOExamsR) = i18nCrumb MsgMenuExamOfficeExams Nothing
|
||||||
@ -1793,13 +1849,13 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
guardM . lift . runDB $
|
guardM . lift . runDB $
|
||||||
(&&) <$> fmap isJust (get ssh)
|
(&&) <$> fmap isJust (get ssh)
|
||||||
<*> fmap isJust (get tid)
|
<*> fmap isJust (get tid)
|
||||||
return (original $ unSchoolKey ssh, Just $ TermCourseListR tid)
|
return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid)
|
||||||
|
|
||||||
breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR
|
breadcrumb AllocationListR = i18nCrumb MsgAllocationListTitle $ Just HomeR
|
||||||
breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do
|
breadcrumb (AllocationR tid ssh ash AShowR) = maybeT (i18nCrumb MsgBreadcrumbAllocation $ Just AllocationListR) $ do
|
||||||
mr <- getMessageRender
|
mr <- getMessageRender
|
||||||
Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash
|
Entity _ Allocation{allocationName} <- MaybeT . runDB . getBy $ TermSchoolAllocationShort tid ssh ash
|
||||||
return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{original (unSchoolKey ssh)})|], Just $ AllocationListR)
|
return ([st|#{allocationName} (#{mr (ShortTermIdentifier (unTermKey tid))}, #{CI.original (unSchoolKey ssh)})|], Just $ AllocationListR)
|
||||||
breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR
|
breadcrumb (AllocationR tid ssh ash ARegisterR) = i18nCrumb MsgBreadcrumbAllocationRegister . Just $ AllocationR tid ssh ash AShowR
|
||||||
breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do
|
breadcrumb (AllocationR tid ssh ash (AApplyR cID)) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ AllocationR tid ssh ash AShowR) $ do
|
||||||
cid <- decrypt cID
|
cid <- decrypt cID
|
||||||
@ -1807,13 +1863,16 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash
|
aid <- MaybeT . getKeyBy $ TermSchoolAllocationShort tid ssh ash
|
||||||
guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ]
|
guardM . lift $ exists [ AllocationCourseAllocation ==. aid, AllocationCourseCourse ==. cid ]
|
||||||
MaybeT $ get cid
|
MaybeT $ get cid
|
||||||
return (original courseName, Just $ AllocationR tid ssh ash AShowR)
|
return (CI.original courseName, Just $ AllocationR tid ssh ash AShowR)
|
||||||
|
|
||||||
|
breadcrumb ParticipantsListR = i18nCrumb MsgBreadcrumbParticipantsList $ Just CourseListR
|
||||||
|
breadcrumb (ParticipantsR _ _) = i18nCrumb MsgBreadcrumbParticipants $ Just ParticipantsListR
|
||||||
|
|
||||||
breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing
|
breadcrumb CourseListR = i18nCrumb MsgMenuCourseList Nothing
|
||||||
breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR
|
breadcrumb CourseNewR = i18nCrumb MsgMenuCourseNew $ Just CourseListR
|
||||||
breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do
|
breadcrumb (CourseR tid ssh csh CShowR) = maybeT (i18nCrumb MsgBreadcrumbCourse . Just $ TermSchoolCourseListR tid ssh) $ do
|
||||||
guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh
|
guardM . lift . runDB . existsBy $ TermSchoolCourseShort tid ssh csh
|
||||||
return (original csh, Just $ TermSchoolCourseListR tid ssh)
|
return (CI.original csh, Just $ TermSchoolCourseListR tid ssh)
|
||||||
breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR
|
breadcrumb (CourseR tid ssh csh CEditR) = i18nCrumb MsgMenuCourseEdit . Just $ CourseR tid ssh csh CShowR
|
||||||
breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR
|
breadcrumb (CourseR tid ssh csh CUsersR) = i18nCrumb MsgMenuCourseMembers . Just $ CourseR tid ssh csh CShowR
|
||||||
breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR
|
breadcrumb (CourseR tid ssh csh CAddUserR) = i18nCrumb MsgMenuCourseAddMembers . Just $ CourseR tid ssh csh CUsersR
|
||||||
@ -1871,7 +1930,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of
|
breadcrumb (CourseR tid ssh csh (ExamR examn sRoute)) = case sRoute of
|
||||||
EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do
|
EShowR -> maybeT (i18nCrumb MsgBreadcrumbExam . Just $ CourseR tid ssh csh CExamListR) $ do
|
||||||
guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR
|
guardM . hasReadAccessTo $ CExamR tid ssh csh examn EShowR
|
||||||
return (original examn, Just $ CourseR tid ssh csh CExamListR)
|
return (CI.original examn, Just $ CourseR tid ssh csh CExamListR)
|
||||||
EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR
|
EEditR -> i18nCrumb MsgMenuExamEdit . Just $ CExamR tid ssh csh examn EShowR
|
||||||
EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR
|
EUsersR -> i18nCrumb MsgMenuExamUsers . Just $ CExamR tid ssh csh examn EShowR
|
||||||
EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR
|
EAddUserR -> i18nCrumb MsgMenuExamAddMembers . Just $ CExamR tid ssh csh examn EUsersR
|
||||||
@ -1885,7 +1944,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
|
breadcrumb (CourseR tid ssh csh (TutorialR tutn sRoute)) = case sRoute of
|
||||||
TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
|
TUsersR -> maybeT (i18nCrumb MsgBreadcrumbTutorial . Just $ CourseR tid ssh csh CTutorialListR) $ do
|
||||||
guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR
|
guardM . hasReadAccessTo $ CTutorialR tid ssh csh tutn TUsersR
|
||||||
return (original tutn, Just $ CourseR tid ssh csh CTutorialListR)
|
return (CI.original tutn, Just $ CourseR tid ssh csh CTutorialListR)
|
||||||
TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR
|
TEditR -> i18nCrumb MsgMenuTutorialEdit . Just $ CTutorialR tid ssh csh tutn TUsersR
|
||||||
TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR
|
TDeleteR -> i18nCrumb MsgMenuTutorialDelete . Just $ CTutorialR tid ssh csh tutn TUsersR
|
||||||
TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR
|
TCommR -> i18nCrumb MsgMenuTutorialComm . Just $ CTutorialR tid ssh csh tutn TUsersR
|
||||||
@ -1895,7 +1954,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of
|
breadcrumb (CourseR tid ssh csh (SheetR shn sRoute)) = case sRoute of
|
||||||
SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do
|
SShowR -> maybeT (i18nCrumb MsgBreadcrumbSheet . Just $ CourseR tid ssh csh SheetListR) $ do
|
||||||
guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR
|
guardM . hasReadAccessTo $ CSheetR tid ssh csh shn SShowR
|
||||||
return (original shn, Just $ CourseR tid ssh csh SheetListR)
|
return (CI.original shn, Just $ CourseR tid ssh csh SheetListR)
|
||||||
SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR
|
SEditR -> i18nCrumb MsgMenuSheetEdit . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR
|
SDelR -> i18nCrumb MsgMenuSheetDelete . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR
|
SSubsR -> i18nCrumb MsgMenuSubmissions . Just $ CSheetR tid ssh csh shn SShowR
|
||||||
@ -1928,7 +1987,7 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of
|
breadcrumb (CourseR tid ssh csh (MaterialR mnm sRoute)) = case sRoute of
|
||||||
MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do
|
MShowR -> maybeT (i18nCrumb MsgBreadcrumbMaterial . Just $ CourseR tid ssh csh MaterialListR) $ do
|
||||||
guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR
|
guardM . hasReadAccessTo $ CMaterialR tid ssh csh mnm MShowR
|
||||||
return (original mnm, Just $ CourseR tid ssh csh MaterialListR)
|
return (CI.original mnm, Just $ CourseR tid ssh csh MaterialListR)
|
||||||
MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR
|
MEditR -> i18nCrumb MsgMenuMaterialEdit . Just $ CMaterialR tid ssh csh mnm MShowR
|
||||||
MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR
|
MDelR -> i18nCrumb MsgMenuMaterialDelete . Just $ CMaterialR tid ssh csh mnm MShowR
|
||||||
MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR
|
MArchiveR -> i18nCrumb MsgBreadcrumbMaterialArchive . Just $ CMaterialR tid ssh csh mnm MShowR
|
||||||
@ -1950,6 +2009,26 @@ instance YesodBreadcrumbs UniWorX where
|
|||||||
breadcrumb MessageListR = i18nCrumb MsgMenuMessageList $ Just AdminR
|
breadcrumb MessageListR = i18nCrumb MsgMenuMessageList $ Just AdminR
|
||||||
|
|
||||||
breadcrumb GlossaryR = i18nCrumb MsgMenuGlossary $ Just InfoR
|
breadcrumb GlossaryR = i18nCrumb MsgMenuGlossary $ Just InfoR
|
||||||
|
|
||||||
|
breadcrumb EExamListR = i18nCrumb MsgMenuExternalExamList Nothing
|
||||||
|
breadcrumb EExamNewR = do
|
||||||
|
isEO <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||||
|
i18nCrumb MsgBreadcrumbExternalExamNew . Just $ if
|
||||||
|
| isEO -> ExamOfficeR EOExamsR
|
||||||
|
| otherwise -> EExamListR
|
||||||
|
breadcrumb (EExamR tid ssh coursen examn sRoute) = case sRoute of
|
||||||
|
EEShowR -> do
|
||||||
|
isEO <- hasReadAccessTo $ ExamOfficeR EOExamsR
|
||||||
|
maybeT (i18nCrumb MsgBreadcrumbExternalExam . Just $ bool EExamListR (ExamOfficeR EOExamsR) isEO) $ do
|
||||||
|
guardM . hasReadAccessTo $ EExamR tid ssh coursen examn EEShowR
|
||||||
|
i18nCrumb (MsgBreadcrumbExternalExamShow coursen examn) . Just $ if
|
||||||
|
| isEO -> ExamOfficeR EOExamsR
|
||||||
|
| otherwise -> EExamListR
|
||||||
|
EEEditR -> i18nCrumb MsgBreadcrumbExternalExamEdit . Just $ EExamR tid ssh coursen examn EEShowR
|
||||||
|
EEUsersR -> i18nCrumb MsgBreadcrumbExternalExamUsers . Just $ EExamR tid ssh coursen examn EEShowR
|
||||||
|
EEGradesR -> i18nCrumb MsgBreadcrumbExternalExamGrades . Just $ EExamR tid ssh coursen examn EEShowR
|
||||||
|
EEStaffInviteR -> i18nCrumb MsgBreadcrumbExternalExamStaffInvite . Just $ EExamR tid ssh coursen examn EEShowR
|
||||||
|
|
||||||
-- breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all
|
-- breadcrumb _ = return ("Uni2work", Nothing) -- Default is no breadcrumb at all
|
||||||
|
|
||||||
submissionList :: TermId -> CourseShorthand -> SheetName -> UserId -> DB [E.Value SubmissionId]
|
submissionList :: TermId -> CourseShorthand -> SheetName -> UserId -> DB [E.Value SubmissionId]
|
||||||
@ -2136,6 +2215,14 @@ pageActions (HomeR) =
|
|||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionSecondary
|
||||||
|
, menuItemLabel = MsgMenuExternalExamList
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute EExamListR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
, MenuItem
|
, MenuItem
|
||||||
{ menuItemType = PageActionPrime
|
{ menuItemType = PageActionPrime
|
||||||
, menuItemLabel = MsgMenuOpenCourses
|
, menuItemLabel = MsgMenuOpenCourses
|
||||||
@ -2355,6 +2442,14 @@ pageActions TermShowR =
|
|||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuParticipantsList
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute ParticipantsListR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
]
|
]
|
||||||
pageActions (TermCourseListR tid) =
|
pageActions (TermCourseListR tid) =
|
||||||
[ MenuItem
|
[ MenuItem
|
||||||
@ -2411,6 +2506,14 @@ pageActions (CourseListR) =
|
|||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuParticipantsList
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute ParticipantsListR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
]
|
]
|
||||||
pageActions (CourseNewR) = [
|
pageActions (CourseNewR) = [
|
||||||
MenuItem
|
MenuItem
|
||||||
@ -2633,8 +2736,8 @@ pageActions (CourseR tid ssh csh SheetListR) =
|
|||||||
, menuItemLabel = MsgMenuCorrectionsOwn
|
, menuItemLabel = MsgMenuCorrectionsOwn
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
|
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
|
||||||
, ("corrections-school", original $ unSchoolKey ssh)
|
, ("corrections-school", CI.original $ unSchoolKey ssh)
|
||||||
, ("corrections-course", original csh)
|
, ("corrections-course", CI.original csh)
|
||||||
])
|
])
|
||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = do
|
, menuItemAccessCallback' = do
|
||||||
@ -2868,9 +2971,9 @@ pageActions (CSheetR tid ssh csh shn SShowR) =
|
|||||||
, menuItemLabel = MsgMenuCorrectionsOwn
|
, menuItemLabel = MsgMenuCorrectionsOwn
|
||||||
, menuItemIcon = Nothing
|
, menuItemIcon = Nothing
|
||||||
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
|
, menuItemRoute = SomeRoute (CorrectionsR, [ ("corrections-term" , termToText $ unTermKey tid)
|
||||||
, ("corrections-school", original $ unSchoolKey ssh)
|
, ("corrections-school", CI.original $ unSchoolKey ssh)
|
||||||
, ("corrections-course", original csh)
|
, ("corrections-course", CI.original csh)
|
||||||
, ("corrections-sheet" , original shn)
|
, ("corrections-sheet" , CI.original shn)
|
||||||
])
|
])
|
||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh
|
, menuItemAccessCallback' = (== Authorized) <$> evalAccessCorrector tid ssh csh
|
||||||
@ -3080,6 +3183,72 @@ pageActions (CorrectionsGradeR) =
|
|||||||
return $ orOf (traverse . _Value . _submissionModeCorrector) sheets
|
return $ orOf (traverse . _Value . _submissionModeCorrector) sheets
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
pageActions EExamListR =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamNew
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute EExamNewR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions (EExamR tid ssh coursen examn EEShowR) =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamEdit
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEEditR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamUsers
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEUsersR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamGrades
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEGradesR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions (EExamR tid ssh coursen examn EEGradesR) =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamUsers
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEUsersR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions (EExamR tid ssh coursen examn EEUsersR) =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuExternalExamGrades
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ EExamR tid ssh coursen examn EEGradesR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pageActions ParticipantsListR =
|
||||||
|
[ MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgCsvOptions
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute CsvOptionsR
|
||||||
|
, menuItemModal = True
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
|
]
|
||||||
pageActions _ = []
|
pageActions _ = []
|
||||||
|
|
||||||
|
|
||||||
@ -3197,6 +3366,7 @@ routeNormalizers =
|
|||||||
, ncMaterial
|
, ncMaterial
|
||||||
, ncTutorial
|
, ncTutorial
|
||||||
, ncExam
|
, ncExam
|
||||||
|
, ncExternalExam
|
||||||
, verifySubmission
|
, verifySubmission
|
||||||
, verifyCourseApplication
|
, verifyCourseApplication
|
||||||
, verifyCourseNews
|
, verifyCourseNews
|
||||||
@ -3220,7 +3390,7 @@ routeNormalizers =
|
|||||||
|
|
||||||
caseChanged :: (Eq a, Show a) => CI a -> CI a -> MaybeT (WriterT Any DB) ()
|
caseChanged :: (Eq a, Show a) => CI a -> CI a -> MaybeT (WriterT Any DB) ()
|
||||||
caseChanged a b
|
caseChanged a b
|
||||||
| ((/=) `on` original) a b = do
|
| ((/=) `on` CI.original) a b = do
|
||||||
$logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|]
|
$logDebugS "routeNormalizers" [st|#{tshow a} /= #{tshow b}|]
|
||||||
tell $ Any True
|
tell $ Any True
|
||||||
| otherwise = return ()
|
| otherwise = return ()
|
||||||
@ -3265,6 +3435,14 @@ routeNormalizers =
|
|||||||
Entity _ Exam{..} <- MaybeT . $cachedHereBinary (cid, examn) . lift . getBy $ UniqueExam cid examn
|
Entity _ Exam{..} <- MaybeT . $cachedHereBinary (cid, examn) . lift . getBy $ UniqueExam cid examn
|
||||||
caseChanged examn examName
|
caseChanged examn examName
|
||||||
return $ route & typesUsing @RouteChildren @ExamName . filtered (== examn) .~ examName
|
return $ route & typesUsing @RouteChildren @ExamName . filtered (== examn) .~ examName
|
||||||
|
ncExternalExam = maybeOrig $ \route -> do
|
||||||
|
EExamR tid ssh coursen examn _ <- return route
|
||||||
|
Entity _ ExternalExam{..} <- MaybeT . $cachedHereBinary (tid, ssh, coursen, examn) . lift . getBy $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
caseChanged coursen externalExamCourseName
|
||||||
|
caseChanged examn externalExamExamName
|
||||||
|
return $ route
|
||||||
|
& typesUsing @RouteChildren @CourseName . filtered (== coursen) .~ externalExamCourseName
|
||||||
|
& typesUsing @RouteChildren @ExamName . filtered (== examn) .~ externalExamExamName
|
||||||
verifySubmission = maybeOrig $ \route -> do
|
verifySubmission = maybeOrig $ \route -> do
|
||||||
CSubmissionR _tid _ssh _csh _shn cID sr <- return route
|
CSubmissionR _tid _ssh _csh _shn cID sr <- return route
|
||||||
sId <- $cachedHereBinary cID $ decrypt cID
|
sId <- $cachedHereBinary cID $ decrypt cID
|
||||||
@ -3304,7 +3482,8 @@ instance YesodPersistRunner UniWorX where
|
|||||||
return . (, cleanup) $ DBRunner (\act -> $logDebugS "YesodPersist" "runDBRunner" >> runDBRunner act)
|
return . (, cleanup) $ DBRunner (\act -> $logDebugS "YesodPersist" "runDBRunner" >> runDBRunner act)
|
||||||
|
|
||||||
data CampusUserConversionException
|
data CampusUserConversionException
|
||||||
= CampusUserInvalidEmail
|
= CampusUserInvalidIdent
|
||||||
|
| CampusUserInvalidEmail
|
||||||
| CampusUserInvalidDisplayName
|
| CampusUserInvalidDisplayName
|
||||||
| CampusUserInvalidGivenName
|
| CampusUserInvalidGivenName
|
||||||
| CampusUserInvalidSurname
|
| CampusUserInvalidSurname
|
||||||
@ -3318,12 +3497,36 @@ instance Exception CampusUserConversionException
|
|||||||
|
|
||||||
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
embedRenderMessage ''UniWorX ''CampusUserConversionException id
|
||||||
|
|
||||||
upsertCampusUser :: Ldap.AttrList [] -> Creds UniWorX -> DB (Entity User)
|
data UpsertCampusUserMode
|
||||||
upsertCampusUser ldapData Creds{..} = do
|
= UpsertCampusUser
|
||||||
|
| UpsertCampusUserDummy { upsertCampusUserIdent :: UserIdent }
|
||||||
|
| UpsertCampusUserOther { uspertCampusUserIdent :: UserIdent }
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
makeLenses_ ''UpsertCampusUserMode
|
||||||
|
makePrisms ''UpsertCampusUserMode
|
||||||
|
|
||||||
|
_upsertCampusUserMode :: Traversal' (Creds UniWorX) UpsertCampusUserMode
|
||||||
|
_upsertCampusUserMode mMode cs@Creds{..}
|
||||||
|
| credsPlugin == "dummy" = setMode <$> mMode (UpsertCampusUserDummy $ CI.mk credsIdent)
|
||||||
|
| credsPlugin `elem` others = setMode <$> mMode (UpsertCampusUserOther $ CI.mk credsIdent)
|
||||||
|
| otherwise = setMode <$> mMode UpsertCampusUser
|
||||||
|
where
|
||||||
|
setMode UpsertCampusUser
|
||||||
|
= cs{ credsPlugin = "LDAP" }
|
||||||
|
setMode (UpsertCampusUserDummy ident)
|
||||||
|
= cs{ credsPlugin = "dummy", credsIdent = CI.original ident }
|
||||||
|
setMode (UpsertCampusUserOther ident)
|
||||||
|
= cs{ credsPlugin = bool (NonEmpty.head others) credsPlugin (credsPlugin `elem` others), credsIdent = CI.original ident }
|
||||||
|
|
||||||
|
others = "PWHash" :| []
|
||||||
|
|
||||||
|
upsertCampusUser :: UpsertCampusUserMode -> Ldap.AttrList [] -> DB (Entity User)
|
||||||
|
upsertCampusUser plugin ldapData = do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
|
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
|
||||||
|
|
||||||
let
|
let
|
||||||
|
userIdent'' = fold [ v | (k, v) <- ldapData, k == ldapUserPrincipalName ]
|
||||||
userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ]
|
userMatrikelnummer' = fold [ v | (k, v) <- ldapData, k == ldapUserMatriculation ]
|
||||||
userEmail' = fold $ do
|
userEmail' = fold $ do
|
||||||
k' <- toList ldapUserEmail
|
k' <- toList ldapUserEmail
|
||||||
@ -3337,13 +3540,23 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ]
|
userSex' = fold [ v | (k, v) <- ldapData, k == ldapSex ]
|
||||||
|
|
||||||
userAuthentication
|
userAuthentication
|
||||||
| isPWHash = error "PWHash should only work for users that are already known"
|
| is _UpsertCampusUserOther plugin
|
||||||
|
= error "PWHash should only work for users that are already known"
|
||||||
| otherwise = AuthLDAP
|
| otherwise = AuthLDAP
|
||||||
userLastAuthentication = now <$ guard (not isDummy)
|
userLastAuthentication = now <$ guard (isn't _UpsertCampusUserDummy plugin)
|
||||||
|
|
||||||
|
userIdent <- if
|
||||||
|
| [bs] <- userIdent''
|
||||||
|
, Right userIdent' <- CI.mk <$> Text.decodeUtf8' bs
|
||||||
|
, hasn't _upsertCampusUserIdent plugin || has (_upsertCampusUserIdent . only userIdent') plugin
|
||||||
|
-> return userIdent'
|
||||||
|
| Just userIdent' <- plugin ^? _upsertCampusUserIdent
|
||||||
|
-> return userIdent'
|
||||||
|
| otherwise
|
||||||
|
-> throwM CampusUserInvalidIdent
|
||||||
userEmail <- if
|
userEmail <- if
|
||||||
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') userEmail'
|
| userEmail : _ <- mapMaybe (assertM (elem '@') . either (const Nothing) Just . Text.decodeUtf8') userEmail'
|
||||||
-> return $ mk userEmail
|
-> return $ CI.mk userEmail
|
||||||
| otherwise
|
| otherwise
|
||||||
-> throwM CampusUserInvalidEmail
|
-> throwM CampusUserInvalidEmail
|
||||||
userDisplayName' <- if
|
userDisplayName' <- if
|
||||||
@ -3393,8 +3606,7 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
|
|
||||||
let
|
let
|
||||||
newUser = User
|
newUser = User
|
||||||
{ userIdent = mk credsIdent
|
{ userMaxFavourites = userDefaultMaxFavourites
|
||||||
, userMaxFavourites = userDefaultMaxFavourites
|
|
||||||
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
, userMaxFavouriteTerms = userDefaultMaxFavouriteTerms
|
||||||
, userTheme = userDefaultTheme
|
, userTheme = userDefaultTheme
|
||||||
, userDateTimeFormat = userDefaultDateTimeFormat
|
, userDateTimeFormat = userDefaultDateTimeFormat
|
||||||
@ -3422,9 +3634,9 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
, UserSex =. userSex
|
, UserSex =. userSex
|
||||||
, UserLastLdapSynchronisation =. Just now
|
, UserLastLdapSynchronisation =. Just now
|
||||||
] ++
|
] ++
|
||||||
[ UserLastAuthentication =. Just now | not isDummy ]
|
[ UserLastAuthentication =. Just now | isn't _UpsertCampusUserDummy plugin ]
|
||||||
|
|
||||||
user@(Entity userId userRec) <- upsertBy (UniqueAuthentication $ mk credsIdent) newUser userUpdate
|
user@(Entity userId userRec) <- upsertBy (UniqueAuthentication userIdent) newUser userUpdate
|
||||||
unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $
|
unless (validDisplayName userTitle userFirstName userSurname $ userDisplayName userRec) $
|
||||||
update userId [ UserDisplayName =. userDisplayName' ]
|
update userId [ UserDisplayName =. userDisplayName' ]
|
||||||
|
|
||||||
@ -3437,7 +3649,7 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
Right str <- return $ Text.decodeUtf8' v'
|
Right str <- return $ Text.decodeUtf8' v'
|
||||||
return str
|
return str
|
||||||
|
|
||||||
termNames = nubBy ((==) `on` mk) $ do
|
termNames = nubBy ((==) `on` CI.mk) $ do
|
||||||
(k, v) <- ldapData
|
(k, v) <- ldapData
|
||||||
guard $ k == ldapUserFieldName
|
guard $ k == ldapUserFieldName
|
||||||
v' <- v
|
v' <- v
|
||||||
@ -3508,7 +3720,7 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
$logDebugS "Campus" [st|Ignoring subterm “#{tshow subterm}”|]
|
$logDebugS "Campus" [st|Ignoring subterm “#{tshow subterm}”|]
|
||||||
assimilateSubTerms subterms unusedFeats
|
assimilateSubTerms subterms unusedFeats
|
||||||
$logDebugS "Campus" [st|Terms for “#{credsIdent}”: #{tshow (sts, fs')}|]
|
$logDebugS "Campus" [st|Terms for “#{userIdent}”: #{tshow (sts, fs')}|]
|
||||||
(fs, studyFieldParentCandidates) <- runWriterT $ assimilateSubTerms sts fs'
|
(fs, studyFieldParentCandidates) <- runWriterT $ assimilateSubTerms sts fs'
|
||||||
|
|
||||||
let
|
let
|
||||||
@ -3614,8 +3826,6 @@ upsertCampusUser ldapData Creds{..} = do
|
|||||||
return user
|
return user
|
||||||
where
|
where
|
||||||
insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ())
|
insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ())
|
||||||
isDummy = credsPlugin == "dummy"
|
|
||||||
isPWHash = credsPlugin == "PWHash"
|
|
||||||
|
|
||||||
associateUserSchoolsByTerms :: UserId -> DB ()
|
associateUserSchoolsByTerms :: UserId -> DB ()
|
||||||
associateUserSchoolsByTerms uid = do
|
associateUserSchoolsByTerms uid = do
|
||||||
@ -3691,18 +3901,18 @@ instance YesodAuth UniWorX where
|
|||||||
setTitleI MsgLoginTitle
|
setTitleI MsgLoginTitle
|
||||||
$(widgetFile "login")
|
$(widgetFile "login")
|
||||||
|
|
||||||
authenticate Creds{..} = liftHandler . runDB $ do
|
authenticate creds@Creds{..} = liftHandler . runDB $ do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
let
|
let
|
||||||
userIdent = mk credsIdent
|
uAuth = UniqueAuthentication $ CI.mk credsIdent
|
||||||
uAuth = UniqueAuthentication userIdent
|
upsertMode = creds ^? _upsertCampusUserMode
|
||||||
|
|
||||||
isDummy = credsPlugin == "dummy"
|
isDummy = is (_Just . _UpsertCampusUserDummy) upsertMode
|
||||||
isPWHash = credsPlugin == "PWHash"
|
isOther = is (_Just . _UpsertCampusUserOther) upsertMode
|
||||||
|
|
||||||
excRecovery res
|
excRecovery res
|
||||||
| isDummy || isPWHash
|
| isDummy || isOther
|
||||||
= do
|
= do
|
||||||
case res of
|
case res of
|
||||||
UserError err -> addMessageI Error err
|
UserError err -> addMessageI Error err
|
||||||
@ -3746,12 +3956,12 @@ instance YesodAuth UniWorX where
|
|||||||
UniWorX{ appSettings' = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod
|
UniWorX{ appSettings' = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod
|
||||||
|
|
||||||
flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of
|
flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of
|
||||||
Just (ldapConf, ldapPool) -> do
|
Just (ldapConf, ldapPool)
|
||||||
let userCreds = Creds credsPlugin (original userIdent) credsExtra
|
| Just upsertMode' <- upsertMode -> do
|
||||||
ldapData <- campusUser ldapConf ldapPool userCreds
|
ldapData <- campusUser ldapConf ldapPool Creds{..}
|
||||||
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
|
$logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData
|
||||||
Authenticated . entityKey <$> upsertCampusUser ldapData userCreds
|
Authenticated . entityKey <$> upsertCampusUser upsertMode' ldapData
|
||||||
Nothing
|
_other
|
||||||
-> acceptExisting
|
-> acceptExisting
|
||||||
|
|
||||||
authPlugins (UniWorX{ appSettings' = AppSettings{..}, appLdapPool }) = catMaybes
|
authPlugins (UniWorX{ appSettings' = AppSettings{..}, appLdapPool }) = catMaybes
|
||||||
|
|||||||
@ -31,6 +31,7 @@ deriving instance Generic SubmissionR
|
|||||||
deriving instance Generic MaterialR
|
deriving instance Generic MaterialR
|
||||||
deriving instance Generic TutorialR
|
deriving instance Generic TutorialR
|
||||||
deriving instance Generic ExamR
|
deriving instance Generic ExamR
|
||||||
|
deriving instance Generic EExamR
|
||||||
deriving instance Generic CourseApplicationR
|
deriving instance Generic CourseApplicationR
|
||||||
deriving instance Generic AllocationR
|
deriving instance Generic AllocationR
|
||||||
deriving instance Generic SchoolR
|
deriving instance Generic SchoolR
|
||||||
|
|||||||
@ -75,7 +75,7 @@ lecturerInvitationConfig = InvitationConfig{..}
|
|||||||
where
|
where
|
||||||
toJunction jLecturerType = (JunctionLecturer{..}, ())
|
toJunction jLecturerType = (JunctionLecturer{..}, ())
|
||||||
lFs = fslI MsgLecturerType & setTooltip MsgCourseLecturerRightsIdentical
|
lFs = fslI MsgLecturerType & setTooltip MsgCourseLecturerRightsIdentical
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ Course{..}) (Entity _ Lecturer{..}) = do
|
invitationSuccessMsg (Entity _ Course{..}) (Entity _ Lecturer{..}) = do
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
return . SomeMessage $ MsgLecturerInvitationAccepted (mr lecturerType) courseShorthand
|
return . SomeMessage $ MsgLecturerInvitationAccepted (mr lecturerType) courseShorthand
|
||||||
|
|||||||
@ -91,7 +91,7 @@ participantInvitationConfig = InvitationConfig{..}
|
|||||||
studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid)
|
studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid)
|
||||||
(fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing
|
(fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing
|
||||||
return . fmap (, ()) $ JunctionParticipant <$> pure now <*> studyFeatures <*> pure Nothing
|
return . fmap (, ()) $ JunctionParticipant <$> pure now <*> studyFeatures <*> pure Nothing
|
||||||
invitationInsertHook _ _ CourseParticipant{..} _ act = do
|
invitationInsertHook _ _ _ CourseParticipant{..} _ act = do
|
||||||
res <- act
|
res <- act
|
||||||
audit $ TransactionCourseParticipantEdit courseParticipantCourse courseParticipantUser
|
audit $ TransactionCourseParticipantEdit courseParticipantCourse courseParticipantUser
|
||||||
return res
|
return res
|
||||||
|
|||||||
@ -71,7 +71,7 @@ examCorrectorInvitationConfig = InvitationConfig{..}
|
|||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionExamCorrector, ())
|
invitationForm _ _ _ = pure (JunctionExamCorrector, ())
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInvitationAccepted examName
|
invitationSuccessMsg (Entity _ Exam{..}) _ = return . SomeMessage $ MsgExamCorrectorInvitationAccepted examName
|
||||||
invitationUltDest (Entity _ Exam{..}) _ = do
|
invitationUltDest (Entity _ Exam{..}) _ = do
|
||||||
Course{..} <- get404 examCourse
|
Course{..} <- get404 examCourse
|
||||||
|
|||||||
@ -97,7 +97,7 @@ examRegistrationInvitationConfig = InvitationConfig{..}
|
|||||||
fieldRes <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) (fslI MsgCourseStudyFeature) Nothing
|
fieldRes <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) (fslI MsgCourseStudyFeature) Nothing
|
||||||
return $ (JunctionExamRegistration invDBExamRegistrationOccurrence now, ) . Just <$> fieldRes
|
return $ (JunctionExamRegistration invDBExamRegistrationOccurrence now, ) . Just <$> fieldRes
|
||||||
(True , _ ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, Nothing)
|
(True , _ ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, Nothing)
|
||||||
invitationInsertHook (Entity eid Exam{..}) _ ExamRegistration{..} mField act = do
|
invitationInsertHook _ (Entity eid Exam{..}) _ ExamRegistration{..} mField act = do
|
||||||
whenIsJust mField $ \cpField -> do
|
whenIsJust mField $ \cpField -> do
|
||||||
insert_ $ CourseParticipant examCourse examRegistrationUser examRegistrationTime cpField Nothing
|
insert_ $ CourseParticipant examCourse examRegistrationUser examRegistrationTime cpField Nothing
|
||||||
queueDBJob . JobQueueNotification $ NotificationCourseRegistered examRegistrationUser examCourse
|
queueDBJob . JobQueueNotification $ NotificationCourseRegistered examRegistrationUser examCourse
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Import hiding ((<.), (.>))
|
|||||||
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Exam
|
import Handler.Utils.Exam
|
||||||
|
import Handler.Utils.Users
|
||||||
import Handler.Utils.Csv
|
import Handler.Utils.Csv
|
||||||
|
|
||||||
import Handler.ExamOffice.Exam (examCloseWidget)
|
import Handler.ExamOffice.Exam (examCloseWidget)
|
||||||
@ -372,7 +373,7 @@ data ExamUserCsvAction
|
|||||||
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
deriveJSON defaultOptions
|
deriveJSON defaultOptions
|
||||||
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel
|
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 3 . dropEnd 1 . splitCamel
|
||||||
, fieldLabelModifier = camelToPathPiece' 3
|
, fieldLabelModifier = camelToPathPiece' 4
|
||||||
, sumEncoding = TaggedObject "action" "data"
|
, sumEncoding = TaggedObject "action" "data"
|
||||||
} ''ExamUserCsvAction
|
} ''ExamUserCsvAction
|
||||||
|
|
||||||
@ -624,7 +625,7 @@ postEUsersR tid ssh csh examn = do
|
|||||||
<*> preview (resultCourseNote . _entityVal . _courseUserNoteNote)
|
<*> preview (resultCourseNote . _entityVal . _courseUserNoteNote)
|
||||||
dbtCsvDecode = Just DBTCsvDecode
|
dbtCsvDecode = Just DBTCsvDecode
|
||||||
{ dbtCsvRowKey = \csv -> do
|
{ dbtCsvRowKey = \csv -> do
|
||||||
uid <- lift $ view _2 <$> guessUser csv
|
uid <- lift $ view _2 <$> guessUser' csv
|
||||||
fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid
|
fmap E.Value . MaybeT . getKeyBy $ UniqueExamRegistration eid uid
|
||||||
, dbtCsvComputeActions = \case
|
, dbtCsvComputeActions = \case
|
||||||
DBCsvDiffMissing{dbCsvOldKey}
|
DBCsvDiffMissing{dbCsvOldKey}
|
||||||
@ -632,7 +633,7 @@ postEUsersR tid ssh csh examn = do
|
|||||||
DBCsvDiffNew{dbCsvNewKey = Just _}
|
DBCsvDiffNew{dbCsvNewKey = Just _}
|
||||||
-> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
|
-> error "An UniqueExamRegistration could be found, but the ExamRegistrationKey is not among the existing keys"
|
||||||
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
|
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
|
||||||
(isPart, uid) <- lift $ guessUser dbCsvNew
|
(isPart, uid) <- lift $ guessUser' dbCsvNew
|
||||||
if
|
if
|
||||||
| isPart -> do
|
| isPart -> do
|
||||||
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
|
yieldM $ ExamUserCsvRegisterData <$> pure uid <*> lookupOccurrence dbCsvNew
|
||||||
@ -930,37 +931,17 @@ postEUsersR tid ssh csh examn = do
|
|||||||
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
|
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
|
||||||
where
|
where
|
||||||
Entity _ User{..} = view resultUser $ existing ! registration
|
Entity _ User{..} = view resultUser $ existing ! registration
|
||||||
|
|
||||||
guessUser :: ExamUserTableCsv -> DB (Bool, UserId)
|
guessUser' :: ExamUserTableCsv -> DB (Bool, UserId)
|
||||||
guessUser ExamUserTableCsv{..} = $cachedHereBinary (csvEUserMatriculation, csvEUserName, csvEUserSurname) $ do
|
guessUser' ExamUserTableCsv{..} = do
|
||||||
users <- E.select . E.from $ \user -> do
|
let criteria = Set.fromList $ catMaybes
|
||||||
E.where_ . E.or $ catMaybes
|
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
||||||
[ (user E.^. UserMatrikelnummer E.==.) . E.val . Just <$> csvEUserMatriculation
|
, GuessUserDisplayName <$> csvEUserName
|
||||||
, (user E.^. UserDisplayName `E.hasInfix`) . E.val <$> csvEUserName
|
, GuessUserSurname <$> csvEUserSurname
|
||||||
, (user E.^. UserSurname `E.hasInfix`) . E.val <$> csvEUserSurname
|
, GuessUserFirstName <$> csvEUserFirstName
|
||||||
, (user E.^. UserFirstName `E.hasInfix`) . E.val <$> csvEUserFirstName
|
|
||||||
]
|
|
||||||
let isCourseParticipant = E.exists . E.from $ \courseParticipant ->
|
|
||||||
E.where_ $ courseParticipant E.^. CourseParticipantCourse E.==. E.val examCourse
|
|
||||||
E.&&. courseParticipant E.^. CourseParticipantUser E.==. user E.^. UserId
|
|
||||||
return (isCourseParticipant, user)
|
|
||||||
let users' = reverse $ sortBy closeness users
|
|
||||||
closeness :: (E.Value Bool, Entity User) -> (E.Value Bool, Entity User) -> Ordering
|
|
||||||
closeness = mconcat $ catMaybes
|
|
||||||
[ pure $ comparing (preview $ _2 . _entityVal . _userMatrikelnummer . only csvEUserMatriculation)
|
|
||||||
, pure $ comparing (view _1)
|
|
||||||
, 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
|
pid <- maybe (throwM ExamUserCsvExceptionNoMatchingUser) return =<< guessUser criteria
|
||||||
[(E.Value isPart, Entity uid _)]
|
(,) <$> existsBy (UniqueParticipant pid examCourse) <*> pure pid
|
||||||
-> return (isPart, uid)
|
|
||||||
(x@(E.Value isPart, Entity uid _) : x' : _)
|
|
||||||
| GT <- x `closeness` x'
|
|
||||||
-> return (isPart, uid)
|
|
||||||
_other
|
|
||||||
-> throwM ExamUserCsvExceptionNoMatchingUser
|
|
||||||
|
|
||||||
lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId)
|
lookupOccurrence :: ExamUserTableCsv -> DB (Maybe ExamOccurrenceId)
|
||||||
lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do
|
lookupOccurrence ExamUserTableCsv{..} = $cachedHereBinary (CI.foldedCase <$> csvEUserOccurrence) . for csvEUserOccurrence $ \occName -> do
|
||||||
@ -971,7 +952,7 @@ postEUsersR tid ssh csh examn = do
|
|||||||
|
|
||||||
lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId)
|
lookupStudyFeatures :: ExamUserTableCsv -> DB (Maybe StudyFeaturesId)
|
||||||
lookupStudyFeatures csv@ExamUserTableCsv{..} = do
|
lookupStudyFeatures csv@ExamUserTableCsv{..} = do
|
||||||
uid <- view _2 <$> guessUser csv
|
uid <- view _2 <$> guessUser' csv
|
||||||
oldFeatures <- getBy $ UniqueParticipant uid examCourse
|
oldFeatures <- getBy $ UniqueParticipant uid examCourse
|
||||||
studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) ->
|
studyFeatures <- E.select . E.from $ \(studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyTerms) ->
|
||||||
E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField)
|
E.distinctOnOrderBy [ E.asc (studyFeatures E.^. StudyFeaturesField)
|
||||||
|
|||||||
@ -6,3 +6,4 @@ import Handler.ExamOffice.Exams as Handler.ExamOffice
|
|||||||
import Handler.ExamOffice.Fields as Handler.ExamOffice
|
import Handler.ExamOffice.Fields as Handler.ExamOffice
|
||||||
import Handler.ExamOffice.Users as Handler.ExamOffice
|
import Handler.ExamOffice.Users as Handler.ExamOffice
|
||||||
import Handler.ExamOffice.Exam as Handler.ExamOffice
|
import Handler.ExamOffice.Exam as Handler.ExamOffice
|
||||||
|
import Handler.ExamOffice.ExternalExam as Handler.ExamOffice
|
||||||
|
|||||||
@ -140,15 +140,15 @@ resultSynchronised :: Traversal' ExamUserTableData (UserDisplayName, UserSurname
|
|||||||
resultSynchronised = _dbrOutput . _9 . traverse
|
resultSynchronised = _dbrOutput . _9 . traverse
|
||||||
|
|
||||||
data ExamUserTableCsv = ExamUserTableCsv
|
data ExamUserTableCsv = ExamUserTableCsv
|
||||||
{ csvEUserSurname :: Text
|
{ csvEUserSurname :: Text
|
||||||
, csvEUserFirstName :: Text
|
, csvEUserFirstName :: Text
|
||||||
, csvEUserName :: Text
|
, csvEUserName :: Text
|
||||||
, csvEUserMatriculation :: Maybe Text
|
, csvEUserMatriculation :: Maybe Text
|
||||||
, csvEUserField :: Maybe Text
|
, csvEUserField :: Maybe Text
|
||||||
, csvEUserDegree :: Maybe Text
|
, csvEUserDegree :: Maybe Text
|
||||||
, csvEUserSemester :: Maybe Int
|
, csvEUserSemester :: Maybe Int
|
||||||
, csvEUserOccurrenceStart :: Maybe ZonedTime
|
, csvEUserOccurrenceStart :: Maybe ZonedTime
|
||||||
, csvEUserExamResult :: ExamResultPassedGrade
|
, csvEUserExamResult :: ExamResultPassedGrade
|
||||||
}
|
}
|
||||||
deriving (Generic)
|
deriving (Generic)
|
||||||
makeLenses_ ''ExamUserTableCsv
|
makeLenses_ ''ExamUserTableCsv
|
||||||
@ -396,7 +396,7 @@ postEGradesR tid ssh csh examn = do
|
|||||||
dbtIdent = "exam-results"
|
dbtIdent = "exam-results"
|
||||||
dbtCsvEncode = Just DBTCsvEncode
|
dbtCsvEncode = Just DBTCsvEncode
|
||||||
{ dbtCsvExportForm = ExamUserCsvExportData
|
{ dbtCsvExportForm = ExamUserCsvExportData
|
||||||
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv) (Just True)
|
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv & setTooltip MsgExamUserMarkSynchronisedCsvTip) (Just False)
|
||||||
, dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do
|
, dbtCsvDoEncode = \ExamUserCsvExportData{..} -> C.mapM $ \(E.Value k, row) -> do
|
||||||
when csvEUserMarkSynchronised $ markSynced k
|
when csvEUserMarkSynchronised $ markSynced k
|
||||||
return $ ExamUserTableCsv
|
return $ ExamUserTableCsv
|
||||||
@ -437,6 +437,7 @@ postEGradesR tid ssh csh examn = do
|
|||||||
whenIsJust usersResult join
|
whenIsJust usersResult join
|
||||||
|
|
||||||
closeWgt <- examCloseWidget (SomeRoute $ CExamR tid ssh csh examn EGradesR) eId
|
closeWgt <- examCloseWidget (SomeRoute $ CExamR tid ssh csh examn EGradesR) eId
|
||||||
|
hasUsers <- hasReadAccessTo $ CExamR tid ssh csh examn EUsersR
|
||||||
|
|
||||||
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do
|
siteLayoutMsg (prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading) $ do
|
||||||
setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading
|
setTitleI $ prependCourseTitle tid ssh csh MsgExamOfficeExamUsersHeading
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Import
|
|||||||
|
|
||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import qualified Handler.Utils.ExamOffice.Exam as Exam
|
import qualified Handler.Utils.ExamOffice.Exam as Exam
|
||||||
|
import qualified Handler.Utils.ExamOffice.ExternalExam as ExternalExam
|
||||||
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
import qualified Database.Esqueleto.Utils as E
|
import qualified Database.Esqueleto.Utils as E
|
||||||
@ -15,60 +16,81 @@ import qualified Database.Esqueleto.Utils as E
|
|||||||
import qualified Colonnade
|
import qualified Colonnade
|
||||||
|
|
||||||
|
|
||||||
type ExamsTableExpr = E.SqlExpr (Entity Exam)
|
type ExamsTableExpr = ( E.SqlExpr (Maybe (Entity Exam))
|
||||||
`E.InnerJoin` E.SqlExpr (Entity Course)
|
`E.InnerJoin` E.SqlExpr (Maybe (Entity Course))
|
||||||
|
)
|
||||||
|
`E.FullOuterJoin` E.SqlExpr (Maybe (Entity ExternalExam))
|
||||||
|
|
||||||
type ExamsTableData = DBRow ( Entity Exam
|
type ExamsTableData = DBRow ( Either (Entity ExternalExam) (Entity Exam, Entity Course)
|
||||||
, Entity Course
|
|
||||||
, Natural, Natural
|
, Natural, Natural
|
||||||
)
|
)
|
||||||
|
|
||||||
queryExam :: Getter ExamsTableExpr (E.SqlExpr (Entity Exam))
|
queryExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Exam)))
|
||||||
queryExam = to $(E.sqlIJproj 2 1)
|
queryExam = to $ $(E.sqlIJproj 2 1) . $(E.sqlFOJproj 2 1)
|
||||||
|
|
||||||
queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Entity Course))
|
queryCourse :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity Course)))
|
||||||
queryCourse = to $(E.sqlIJproj 2 2)
|
queryCourse = to $ $(E.sqlIJproj 2 2) . $(E.sqlFOJproj 2 1)
|
||||||
|
|
||||||
|
queryExternalExam :: Getter ExamsTableExpr (E.SqlExpr (Maybe (Entity ExternalExam)))
|
||||||
|
queryExternalExam = to $(E.sqlFOJproj 2 2)
|
||||||
|
|
||||||
querySynchronised :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
|
querySynchronised :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
|
||||||
querySynchronised office = to . runReader $ do
|
querySynchronised office = to . runReader $ do
|
||||||
exam <- view queryExam
|
exam' <- view queryExam
|
||||||
|
externalExam' <- view queryExternalExam
|
||||||
let
|
let
|
||||||
synchronised = E.subSelectCount . E.from $ \examResult -> do
|
examSynchronised examId = E.subSelectCount . E.from $ \examResult -> do
|
||||||
E.where_ $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
|
E.where_ $ examResult E.^. ExamResultExam E.==. examId
|
||||||
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
||||||
E.where_ $ Exam.resultIsSynced office examResult
|
E.where_ $ Exam.resultIsSynced office examResult
|
||||||
return synchronised
|
externalExamSynchronised externalExamId = E.subSelectCount . E.from $ \externalExamResult -> do
|
||||||
|
E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
|
||||||
|
E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
|
||||||
|
E.where_ $ ExternalExam.resultIsSynced office externalExamResult
|
||||||
|
return $ E.maybe (E.val 0) examSynchronised (exam' E.?. ExamId) E.+. E.maybe (E.val 0) externalExamSynchronised (externalExam' E.?. ExternalExamId)
|
||||||
|
|
||||||
queryResults :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
|
queryResults :: E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Natural))
|
||||||
queryResults office = to . runReader $ do
|
queryResults office = to . runReader $ do
|
||||||
exam <- view queryExam
|
exam' <- view queryExam
|
||||||
|
externalExam' <- view queryExternalExam
|
||||||
let
|
let
|
||||||
results = E.subSelectCount . E.from $ \examResult -> do
|
results examId = E.subSelectCount . E.from $ \examResult -> do
|
||||||
E.where_ $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
|
E.where_ $ examResult E.^. ExamResultExam E.==. examId
|
||||||
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
||||||
return results
|
externalResults externalExamId = E.subSelectCount . E.from $ \externalExamResult -> do
|
||||||
|
E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
|
||||||
|
E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
|
||||||
|
return $ E.maybe (E.val 0) results (exam' E.?. ExamId) E.+. E.maybe (E.val 0) externalResults (externalExam' E.?. ExternalExamId)
|
||||||
|
|
||||||
queryIsSynced :: UTCTime -> E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Bool))
|
queryIsSynced :: UTCTime -> E.SqlExpr (E.Value UserId) -> Getter ExamsTableExpr (E.SqlExpr (E.Value Bool))
|
||||||
queryIsSynced now office = to . runReader $ do
|
queryIsSynced now office = to . runReader $ do
|
||||||
exam <- view queryExam
|
exam' <- view queryExam
|
||||||
|
externalExam' <- view queryExternalExam
|
||||||
let
|
let
|
||||||
synchronised = E.not_ . E.exists . E.from $ \examResult -> do
|
examSynchronised examId = E.not_ . E.exists . E.from $ \examResult -> do
|
||||||
E.where_ $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
|
E.where_ $ examResult E.^. ExamResultExam E.==. examId
|
||||||
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
E.where_ $ Exam.examOfficeExamResultAuth office examResult
|
||||||
E.where_ . E.not_ $ Exam.resultIsSynced office examResult
|
E.where_ . E.not_ $ Exam.resultIsSynced office examResult
|
||||||
open = E.maybe E.true (E.>. E.val now) $ exam E.^. ExamClosed
|
externalExamSynchronised externalExamId = E.not_ . E.exists . E.from $ \externalExamResult -> do
|
||||||
return $ synchronised E.||. open
|
E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. externalExamId
|
||||||
|
E.where_ $ ExternalExam.examOfficeExternalExamResultAuth office externalExamResult
|
||||||
|
E.where_ . E.not_ $ ExternalExam.resultIsSynced office externalExamResult
|
||||||
|
open examClosed' = E.maybe E.true (E.>. E.val now) $ examClosed'
|
||||||
|
return $ E.maybe E.false examSynchronised (exam' E.?. ExamId) E.||. E.maybe E.false open (exam' E.?. ExamClosed) E.||. E.maybe E.false externalExamSynchronised (externalExam' E.?. ExternalExamId)
|
||||||
|
|
||||||
|
|
||||||
resultExam :: Lens' ExamsTableData (Entity Exam)
|
resultExam :: Traversal' ExamsTableData (Entity Exam)
|
||||||
resultExam = _dbrOutput . _1
|
resultExam = _dbrOutput . _1 . _Right . _1
|
||||||
|
|
||||||
resultCourse :: Lens' ExamsTableData (Entity Course)
|
resultCourse :: Traversal' ExamsTableData (Entity Course)
|
||||||
resultCourse = _dbrOutput . _2
|
resultCourse = _dbrOutput . _1 . _Right . _2
|
||||||
|
|
||||||
|
resultExternalExam :: Traversal' ExamsTableData (Entity ExternalExam)
|
||||||
|
resultExternalExam = _dbrOutput . _1 . _Left
|
||||||
|
|
||||||
resultSynchronised, resultResults :: Lens' ExamsTableData Natural
|
resultSynchronised, resultResults :: Lens' ExamsTableData Natural
|
||||||
resultSynchronised = _dbrOutput . _3
|
resultSynchronised = _dbrOutput . _2
|
||||||
resultResults = _dbrOutput . _4
|
resultResults = _dbrOutput . _3
|
||||||
|
|
||||||
resultIsSynced :: Getter ExamsTableData Bool
|
resultIsSynced :: Getter ExamsTableData Bool
|
||||||
resultIsSynced = to $ (>=) <$> view resultSynchronised <*> view resultResults
|
resultIsSynced = to $ (>=) <$> view resultSynchronised <*> view resultResults
|
||||||
@ -90,6 +112,10 @@ getEOExamsR = do
|
|||||||
courseLink :: Course -> SomeRoute UniWorX
|
courseLink :: Course -> SomeRoute UniWorX
|
||||||
courseLink Course{..}
|
courseLink Course{..}
|
||||||
= SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR
|
= SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR
|
||||||
|
|
||||||
|
externalExamLink :: ExternalExam -> SomeRoute UniWorX
|
||||||
|
externalExamLink ExternalExam{..}
|
||||||
|
= SomeRoute $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEGradesR
|
||||||
|
|
||||||
querySynchronised' = querySynchronised $ E.val uid
|
querySynchronised' = querySynchronised $ E.val uid
|
||||||
queryResults' = queryResults $ E.val uid
|
queryResults' = queryResults $ E.val uid
|
||||||
@ -100,34 +126,48 @@ getEOExamsR = do
|
|||||||
dbtSQLQuery = runReaderT $ do
|
dbtSQLQuery = runReaderT $ do
|
||||||
exam <- view queryExam
|
exam <- view queryExam
|
||||||
course <- view queryCourse
|
course <- view queryCourse
|
||||||
|
externalExam <- view queryExternalExam
|
||||||
|
|
||||||
synchronised <- view querySynchronised'
|
synchronised <- view querySynchronised'
|
||||||
results <- view queryResults'
|
results <- view queryResults'
|
||||||
|
|
||||||
lift $ do
|
lift $ do
|
||||||
E.on $ exam E.^. ExamCourse E.==. course E.^. CourseId
|
E.on E.false
|
||||||
|
E.on $ exam E.?. ExamCourse E.==. course E.?. CourseId
|
||||||
|
|
||||||
E.where_ $ results E.>. E.val 0
|
E.where_ $ results E.>. E.val 0
|
||||||
|
E.where_ $ (E.not_ (E.isNothing $ exam E.?. ExamId) E.&&. E.not_ (E.isNothing $ course E.?. CourseId) E.&&. E.isNothing (externalExam E.?. ExternalExamId))
|
||||||
|
E.||. ( E.isNothing (exam E.?. ExamId) E.&&. E.isNothing (course E.?. CourseId) E.&&. E.not_ (E.isNothing $ externalExam E.?. ExternalExamId))
|
||||||
|
|
||||||
return (exam, course, synchronised, results)
|
return (exam, course, externalExam, synchronised, results)
|
||||||
dbtRowKey = views queryExam (E.^. ExamId)
|
dbtRowKey = views ($(multifocusG 2) queryExam queryExternalExam) (bimap (E.?. ExamId) (E.?. ExternalExamId))
|
||||||
|
|
||||||
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) ExamsTableData
|
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) ExamsTableData
|
||||||
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ do
|
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $ do
|
||||||
exam <- view $ _1 . _entityVal
|
exam <- view _1
|
||||||
course <- view $ _2 . _entityVal
|
course <- view _2
|
||||||
|
externalExam <- view _3
|
||||||
|
|
||||||
guard =<< hasReadAccessTo (urlRoute $ examLink course exam)
|
case (exam, course, externalExam) of
|
||||||
|
(Just exam', Just course', Nothing) -> do
|
||||||
|
guard =<< hasReadAccessTo (urlRoute $ examLink (entityVal course') (entityVal exam'))
|
||||||
|
|
||||||
(,,,)
|
(,,)
|
||||||
<$> view _1 <*> view _2 <*> view (_3 . _Value) <*> view (_4 . _Value)
|
<$> pure (Right (exam', course')) <*> view (_4 . _Value) <*> view (_5 . _Value)
|
||||||
|
(Nothing, Nothing, Just externalExam') -> do
|
||||||
|
guard =<< hasReadAccessTo (urlRoute $ externalExamLink (entityVal externalExam'))
|
||||||
|
|
||||||
|
(,,)
|
||||||
|
<$> pure (Left externalExam') <*> view (_4 . _Value) <*> view (_5 . _Value)
|
||||||
|
_other -> return $ error "Got exam & externalExam in same result"
|
||||||
|
|
||||||
|
|
||||||
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
|
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "synced") $ i18nCell MsgExamSynchronised) $ \x -> flip runReader x $ do
|
||||||
Entity _ Exam{examClosed} <- view resultExam
|
mExam <- preview resultExam
|
||||||
|
|
||||||
if
|
if
|
||||||
| NTop examClosed > NTop (Just now)
|
| Just (Entity _ Exam{examClosed}) <- mExam
|
||||||
|
, NTop examClosed > NTop (Just now)
|
||||||
-> return . cell $ toWidget iconNew
|
-> return . cell $ toWidget iconNew
|
||||||
| otherwise
|
| otherwise
|
||||||
-> do
|
-> do
|
||||||
@ -151,26 +191,28 @@ getEOExamsR = do
|
|||||||
dbtColonnade :: Colonnade Sortable _ _
|
dbtColonnade :: Colonnade Sortable _ _
|
||||||
dbtColonnade = mconcat
|
dbtColonnade = mconcat
|
||||||
[ colSynced
|
[ colSynced
|
||||||
, anchorColonnade (views ($(multifocusG 2) (resultCourse . _entityVal) (resultExam . _entityVal)) (uncurry examLink))
|
, maybeAnchorColonnade ( runMaybeT $ mpreview ($(multifocusG 2) (pre $ resultCourse . _entityVal) (pre $ resultExam . _entityVal) . to (uncurry $ liftA2 examLink) . _Just)
|
||||||
$ colExamName (resultExam . _entityVal . _examName)
|
<|> mpreviews (resultExternalExam . _entityVal) externalExamLink
|
||||||
, colExamTime (resultExam . _entityVal . $(multifocusG 2) _examStart _examEnd)
|
)
|
||||||
, colExamFinishedOffice (resultExam . _entityVal . _examFinished)
|
$ emptyOpticColonnade (resultExam . _entityVal . _examName <> resultExternalExam . _entityVal . _externalExamExamName) colExamName
|
||||||
, colExamClosed (resultExam . _entityVal . _examClosed)
|
, emptyOpticColonnade (resultExam . _entityVal . $(multifocusG 2) _examStart _examEnd) colExamTime
|
||||||
, anchorColonnade (views (resultCourse . _entityVal) courseLink)
|
, emptyOpticColonnade (resultExam . _entityVal . _examFinished) colExamFinishedOffice
|
||||||
$ colCourseName (resultCourse . _entityVal . _courseName)
|
, emptyOpticColonnade (resultExam . _entityVal . _examClosed) colExamClosed
|
||||||
, colSchool (resultCourse . _entityVal . _courseSchool)
|
, maybeAnchorColonnade (previews (resultCourse . _entityVal) courseLink)
|
||||||
, colTermShort (resultCourse . _entityVal . _courseTerm)
|
$ emptyOpticColonnade (resultCourse . _entityVal . _courseName <> resultExternalExam . _entityVal . _externalExamCourseName) colCourseName
|
||||||
|
, emptyOpticColonnade (resultCourse . _entityVal . _courseSchool <> resultExternalExam . _entityVal . _externalExamSchool) colSchool
|
||||||
|
, emptyOpticColonnade (resultCourse . _entityVal . _courseTerm <> resultExternalExam . _entityVal . _externalExamTerm) colTermShort
|
||||||
]
|
]
|
||||||
dbtSorting = mconcat
|
dbtSorting = mconcat
|
||||||
[ singletonMap "synced" . SortColumn $ (E./.) <$> view querySynchronised' <*> view queryResults'
|
[ singletonMap "synced" . SortColumn $ (E./.) <$> view querySynchronised' <*> view queryResults'
|
||||||
, singletonMap "is-synced" . SortColumn $ view queryIsSynced'
|
, singletonMap "is-synced" . SortColumn $ view queryIsSynced'
|
||||||
, sortExamName (queryExam . to (E.^. ExamName))
|
, sortExamName (to $ E.unsafeCoalesce . sequence [views queryExam (E.?. ExamName), views queryExternalExam (E.?. ExternalExamExamName)])
|
||||||
, sortExamTime (queryExam . $(multifocusG 2) (to (E.^. ExamStart)) (to (E.^. ExamEnd)))
|
, sortExamTime (queryExam . $(multifocusG 2) (to $ E.joinV . (E.?. ExamStart)) (to $ E.joinV . (E.?. ExamEnd)))
|
||||||
, sortExamFinished (queryExam . to (E.^. ExamFinished))
|
, sortExamFinished (queryExam . to (E.joinV . (E.?. ExamFinished)))
|
||||||
, sortExamClosed (queryExam . to (E.^. ExamClosed))
|
, sortExamClosed (queryExam . to (E.joinV . (E.?. ExamClosed)))
|
||||||
, sortCourseName (queryCourse . to (E.^. CourseName))
|
, sortCourseName (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseName), views queryExternalExam (E.?. ExternalExamCourseName)])
|
||||||
, sortSchool (queryCourse . to (E.^. CourseSchool))
|
, sortSchool (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseSchool), views queryExternalExam (E.?. ExternalExamSchool)])
|
||||||
, sortTerm (queryCourse . to (E.^. CourseTerm))
|
, sortTerm (to $ E.unsafeCoalesce . sequence [views queryCourse (E.?. CourseTerm), views queryExternalExam (E.?. ExternalExamTerm)])
|
||||||
]
|
]
|
||||||
|
|
||||||
dbtFilter = mconcat
|
dbtFilter = mconcat
|
||||||
|
|||||||
33
src/Handler/ExamOffice/ExternalExam.hs
Normal file
33
src/Handler/ExamOffice/ExternalExam.hs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
module Handler.ExamOffice.ExternalExam
|
||||||
|
( getEEGradesR, postEEGradesR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import Handler.Utils.ExternalExam.Users
|
||||||
|
|
||||||
|
getEEGradesR, postEEGradesR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
|
||||||
|
getEEGradesR = postEEGradesR
|
||||||
|
postEEGradesR tid ssh coursen examn = do
|
||||||
|
(usersResult, table) <- runDB $ do
|
||||||
|
eExam <- getBy404 $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
(usersResult, examUsersTable) <- makeExternalExamUsersTable EEUMGrades eExam
|
||||||
|
|
||||||
|
usersResult' <- formResultMaybe usersResult $ \case
|
||||||
|
(ExternalExamUserMarkSynchronisedData, selectedResults) -> do
|
||||||
|
forM_ selectedResults externalExamResultMarkSynchronised
|
||||||
|
return . Just $ do
|
||||||
|
addMessageI Success $ MsgExamUserMarkedSynchronised (length selectedResults)
|
||||||
|
redirect $ EExamR tid ssh coursen examn EEGradesR
|
||||||
|
|
||||||
|
return (usersResult', examUsersTable)
|
||||||
|
|
||||||
|
whenIsJust usersResult join
|
||||||
|
|
||||||
|
hasUsers <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
|
||||||
|
|
||||||
|
siteLayoutMsg (MsgExternalExamGrades coursen examn) $ do
|
||||||
|
setTitleI MsgBreadcrumbExternalExamGrades
|
||||||
|
$(widgetFile "exam-office/externalExamGrades")
|
||||||
@ -74,7 +74,7 @@ examOfficeUserInvitationConfig = InvitationConfig{..}
|
|||||||
return InvitationTokenConfig{..}
|
return InvitationTokenConfig{..}
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionExamOfficeUser, ())
|
invitationForm _ _ _ = pure (JunctionExamOfficeUser, ())
|
||||||
invitationInsertHook _ _ ExamOfficeUser{..} _ act = do
|
invitationInsertHook _ _ _ ExamOfficeUser{..} _ act = do
|
||||||
res <- act
|
res <- act
|
||||||
audit $ TransactionExamOfficeUserAdd examOfficeUserOffice examOfficeUserUser
|
audit $ TransactionExamOfficeUserAdd examOfficeUserOffice examOfficeUserUser
|
||||||
return res
|
return res
|
||||||
|
|||||||
10
src/Handler/ExternalExam.hs
Normal file
10
src/Handler/ExternalExam.hs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module Handler.ExternalExam
|
||||||
|
( module Handler.ExternalExam
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Handler.ExternalExam.List as Handler.ExternalExam
|
||||||
|
import Handler.ExternalExam.New as Handler.ExternalExam
|
||||||
|
import Handler.ExternalExam.Show as Handler.ExternalExam
|
||||||
|
import Handler.ExternalExam.Edit as Handler.ExternalExam
|
||||||
|
import Handler.ExternalExam.Users as Handler.ExternalExam
|
||||||
|
import Handler.ExternalExam.StaffInvite as Handler.ExternalExam
|
||||||
98
src/Handler/ExternalExam/Edit.hs
Normal file
98
src/Handler/ExternalExam/Edit.hs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
module Handler.ExternalExam.Edit
|
||||||
|
( getEEEditR, postEEEditR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
|
||||||
|
import Handler.ExternalExam.Form
|
||||||
|
import Handler.ExternalExam.StaffInvite
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
|
||||||
|
getEEEditR, postEEEditR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
|
||||||
|
getEEEditR = postEEEditR
|
||||||
|
postEEEditR tid ssh coursen examn = do
|
||||||
|
(Entity eeId ExternalExam{..}, schools, staff) <- runDB $ do
|
||||||
|
eExam@(Entity eeId _) <- getBy404 $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
schools <- setOf (folded . _entityVal . _externalExamOfficeSchoolSchool) <$> selectList [ ExternalExamOfficeSchoolExam ==. eeId ] []
|
||||||
|
actualStaff <- selectList [ ExternalExamStaffExam ==. eeId ] []
|
||||||
|
invitedStaff <- sourceInvitationsF @ExternalExamStaff eeId
|
||||||
|
let staff = setOf (folded . _entityVal . _externalExamStaffUser . re _Right) actualStaff
|
||||||
|
<> Set.mapMonotonic Left (Map.keysSet invitedStaff)
|
||||||
|
return (eExam, schools, staff)
|
||||||
|
|
||||||
|
let
|
||||||
|
template = ExternalExamForm
|
||||||
|
{ eefTerm = tid
|
||||||
|
, eefSchool = ssh
|
||||||
|
, eefCourseName = coursen
|
||||||
|
, eefExamName = examn
|
||||||
|
, eefDefaultTime = externalExamDefaultTime
|
||||||
|
, eefShowGrades = externalExamShowGrades
|
||||||
|
, eefOfficeSchools = schools
|
||||||
|
, eefStaff = staff
|
||||||
|
}
|
||||||
|
|
||||||
|
((examResult, examWidget'), examEnctype) <- runFormPost . externalExamForm $ Just template
|
||||||
|
|
||||||
|
formResult examResult $ \ExternalExamForm{..} -> do
|
||||||
|
replaceRes <- runDBJobs $ do
|
||||||
|
replaceRes <- replaceUnique eeId ExternalExam
|
||||||
|
{ externalExamTerm = eefTerm
|
||||||
|
, externalExamSchool = eefSchool
|
||||||
|
, externalExamCourseName = eefCourseName
|
||||||
|
, externalExamExamName = eefExamName
|
||||||
|
, externalExamDefaultTime = eefDefaultTime
|
||||||
|
, externalExamShowGrades = eefShowGrades
|
||||||
|
}
|
||||||
|
when (is _Nothing replaceRes) $ do
|
||||||
|
audit $ TransactionExternalExamEdit eeId
|
||||||
|
|
||||||
|
forM_ (eefStaff `setSymmDiff` staff) $ \change -> if
|
||||||
|
| change `Set.member` eefStaff -> case change of
|
||||||
|
Left invEmail -> do
|
||||||
|
audit $ TransactionExternalExamStaffInviteEdit eeId invEmail
|
||||||
|
sinkInvitationsF externalExamStaffInvitationConfig
|
||||||
|
[(invEmail, eeId, (InvDBDataExternalExamStaff, InvTokenDataExternalExamStaff))]
|
||||||
|
Right staffUid -> do
|
||||||
|
audit $ TransactionExternalExamStaffEdit eeId staffUid
|
||||||
|
insert_ $ ExternalExamStaff staffUid eeId
|
||||||
|
| otherwise -> case change of
|
||||||
|
Left invEmail -> do
|
||||||
|
audit $ TransactionExternalExamStaffInviteDelete eeId invEmail
|
||||||
|
deleteInvitation @ExternalExamStaff eeId invEmail
|
||||||
|
Right staffUid -> do
|
||||||
|
audit $ TransactionExternalExamStaffDelete eeId staffUid
|
||||||
|
deleteBy $ UniqueExternalExamStaff eeId staffUid
|
||||||
|
|
||||||
|
forM_ (eefOfficeSchools `setSymmDiff` schools) $ \change -> if
|
||||||
|
| change `Set.member` eefOfficeSchools -> do
|
||||||
|
audit $ TransactionExternalExamOfficeSchoolEdit eeId change
|
||||||
|
insert_ $ ExternalExamOfficeSchool change eeId
|
||||||
|
| otherwise -> do
|
||||||
|
audit $ TransactionExternalExamOfficeSchoolDelete eeId change
|
||||||
|
deleteBy $ UniqueExternalExamOfficeSchool eeId change
|
||||||
|
return replaceRes
|
||||||
|
|
||||||
|
case replaceRes of
|
||||||
|
Nothing -> do
|
||||||
|
addMessageI Success $ MsgExternalExamEdited eefCourseName eefExamName
|
||||||
|
redirect $ EExamR eefTerm eefSchool eefCourseName eefExamName EEShowR
|
||||||
|
Just _ ->
|
||||||
|
addMessageI Error $ MsgExternalExamExists eefCourseName eefExamName
|
||||||
|
|
||||||
|
let heading = MsgExternalExamEdit coursen examn
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
wrapForm examWidget' def
|
||||||
|
{ formAction = Just . SomeRoute $ EExamR tid ssh coursen examn EEEditR
|
||||||
|
, formEncoding = examEnctype
|
||||||
|
}
|
||||||
115
src/Handler/ExternalExam/Form.hs
Normal file
115
src/Handler/ExternalExam/Form.hs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
module Handler.ExternalExam.Form
|
||||||
|
( ExternalExamForm(..)
|
||||||
|
, externalExamForm
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import Handler.ExternalExam.StaffInvite ()
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import Data.Map ((!))
|
||||||
|
|
||||||
|
import qualified Control.Monad.State.Class as State
|
||||||
|
|
||||||
|
|
||||||
|
data ExternalExamForm = ExternalExamForm
|
||||||
|
{ eefTerm :: TermId
|
||||||
|
, eefSchool :: SchoolId
|
||||||
|
, eefCourseName :: CI Text
|
||||||
|
, eefExamName :: CI Text
|
||||||
|
, eefDefaultTime :: Maybe UTCTime
|
||||||
|
, eefShowGrades :: Bool
|
||||||
|
, eefOfficeSchools :: Set SchoolId
|
||||||
|
, eefStaff :: Set (Either UserEmail UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeLenses_ ''ExternalExamForm
|
||||||
|
|
||||||
|
externalExamForm :: Maybe ExternalExamForm -> Form ExternalExamForm
|
||||||
|
externalExamForm template = validateForm validateExternalExam $ \html -> do
|
||||||
|
uid <- requireAuthId
|
||||||
|
cRoute <- fromMaybe (error "tutorialForm called from 404-Handler") <$> getCurrentRoute
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
|
||||||
|
let termsField = case template of
|
||||||
|
Just template' -> termsSetField [eefTerm template']
|
||||||
|
_other -> termsAllowedField
|
||||||
|
|
||||||
|
(lecturerSchools, adminSchools, oldSchool) <- liftHandler . runDB $ do
|
||||||
|
lecturerSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolLecturer]] []
|
||||||
|
protoAdminSchools <- map (userFunctionSchool . entityVal) <$> selectList [UserFunctionUser ==. uid, UserFunctionFunction <-. [SchoolAdmin]] []
|
||||||
|
adminSchools <- filterM (hasWriteAccessTo . flip SchoolR SchoolEditR) protoAdminSchools
|
||||||
|
let oldSchool = eefSchool <$> template
|
||||||
|
return (lecturerSchools, adminSchools, oldSchool)
|
||||||
|
let userSchools = nub . maybe id (:) oldSchool $ lecturerSchools ++ adminSchools
|
||||||
|
|
||||||
|
flip (renderAForm FormStandard) html $ ExternalExamForm
|
||||||
|
<$> areq termsField (fslI MsgExternalExamSemester) (eefTerm <$> template)
|
||||||
|
<*> areq (schoolFieldFor userSchools) (fslI MsgExternalExamSchool) (eefSchool <$> template)
|
||||||
|
<*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamCourseName & setTooltip MsgExternalExamCourseNameTip & addPlaceholder (mr MsgExternalExamCourseNamePlaceholder)) (eefCourseName <$> template)
|
||||||
|
<*> areq (textField & cfStrip & cfCI) (fslI MsgExternalExamExamName & setTooltip MsgExternalExamExamNameTip & addPlaceholder (mr MsgExternalExamExamNamePlaceholder)) (eefExamName <$> template)
|
||||||
|
<*> aopt utcTimeField (fslI MsgExternalExamDefaultTime & setTooltip MsgExternalExamDefaultTimeTip & addPlaceholder (mr MsgExternalExamDefaultTimePlaceholder)) (eefDefaultTime <$> template)
|
||||||
|
<*> apopt checkBoxField (fslI MsgExternalExamShowGrades & setTooltip MsgExternalExamShowGradesTip) (eefShowGrades <$> template)
|
||||||
|
<*> (Set.fromList <$> officeSchoolForm cRoute (Set.toList . eefOfficeSchools <$> template))
|
||||||
|
<*> (Set.fromList <$> staffForm cRoute ((Set.toList . eefStaff <$> template) <|> pure (pure $ Right uid)))
|
||||||
|
where
|
||||||
|
officeSchoolForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
|
||||||
|
where
|
||||||
|
miAdd mkUnique submitView csrf = do
|
||||||
|
(schoolRes, addView) <- mpopt schoolField ("" & addName (mkUnique "school")) Nothing
|
||||||
|
let schoolRes' = schoolRes <&> \newDat oldDat -> FormSuccess (guardOn (newDat `notElem` oldDat) newDat)
|
||||||
|
return (schoolRes', $(widgetFile "external-exam/schoolMassInput/add"))
|
||||||
|
miCell ssh = do
|
||||||
|
School{..} <- liftHandler . runDB $ getJust ssh
|
||||||
|
$(widgetFile "external-exam/schoolMassInput/cell")
|
||||||
|
miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
miButtonAction = Just . SomeRoute . (cRoute :#:)
|
||||||
|
miLayout :: MassInputLayout ListLength SchoolId ()
|
||||||
|
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "external-exam/schoolMassInput/layout")
|
||||||
|
miIdent :: Text
|
||||||
|
miIdent = "external-exams-school"
|
||||||
|
fSettings = fslI MsgExternalExamExamOfficeSchools & setTooltip (UniWorXMessages [SomeMessage MsgExternalExamExamOfficeSchoolsTip, SomeMessage MsgMassInputTip])
|
||||||
|
fRequired = False
|
||||||
|
staffForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
|
||||||
|
where
|
||||||
|
miAdd mkUnique submitView csrf = do
|
||||||
|
MsgRenderer mr <- getMsgRenderer
|
||||||
|
(usersRes, addView) <- mpreq (multiUserField False Nothing) ("" & addName (mkUnique "email")) Nothing
|
||||||
|
let
|
||||||
|
usersRes' = usersRes <&> \newDat oldDat -> if
|
||||||
|
| existing <- newDat `Set.intersection` Set.fromList oldDat
|
||||||
|
, not $ Set.null existing
|
||||||
|
-> FormFailure [mr MsgExternalExamStaffAlreadyAdded]
|
||||||
|
| otherwise
|
||||||
|
-> FormSuccess $ Set.toList newDat
|
||||||
|
return (usersRes', $(widgetFile "external-exam/staffMassInput/add"))
|
||||||
|
miCell (Left email) = do
|
||||||
|
invWarnMsg <- messageI Warning MsgEmailInvitationWarning
|
||||||
|
$(widgetFile "external-exam/staffMassInput/cellInvitation")
|
||||||
|
miCell (Right userId) = do
|
||||||
|
User{..} <- liftHandler . runDB $ getJust userId
|
||||||
|
$(widgetFile "external-exam/staffMassInput/cellKnown")
|
||||||
|
miButtonAction :: forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)
|
||||||
|
miButtonAction = Just . SomeRoute . (cRoute :#:)
|
||||||
|
miLayout :: MassInputLayout ListLength (Either UserEmail UserId) ()
|
||||||
|
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "external-exam/staffMassInput/layout")
|
||||||
|
miIdent :: Text
|
||||||
|
miIdent = "external-exams-staff"
|
||||||
|
fSettings = fslI MsgExternalExamStaff & setTooltip (UniWorXMessages [SomeMessage MsgExternalExamStaffTip, SomeMessage MsgMassInputTip])
|
||||||
|
fRequired = True
|
||||||
|
|
||||||
|
validateExternalExam :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => FormValidator ExternalExamForm m ()
|
||||||
|
validateExternalExam = do
|
||||||
|
State.modify $ \eeForm -> eeForm & over _eefOfficeSchools (Set.delete $ eeForm ^. _eefSchool)
|
||||||
|
|
||||||
|
ExternalExamForm{..} <- State.get
|
||||||
|
|
||||||
|
isAdmin <- hasWriteAccessTo $ SchoolR eefSchool SchoolEditR
|
||||||
|
unless isAdmin $ do
|
||||||
|
uid <- requireAuthId
|
||||||
|
guardValidation MsgExternalExamUserMustBeStaff $ Right uid `Set.member` eefStaff
|
||||||
|
|
||||||
|
courseExists <- liftHandler . runDB . existsBy $ TermSchoolCourseName eefTerm eefSchool eefCourseName
|
||||||
|
guardValidation MsgExternalExamCourseExists $ not courseExists
|
||||||
81
src/Handler/ExternalExam/List.hs
Normal file
81
src/Handler/ExternalExam/List.hs
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
module Handler.ExternalExam.List
|
||||||
|
( getEExamListR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
|
getEExamListR :: Handler Html
|
||||||
|
getEExamListR = do
|
||||||
|
mAuthId <- maybeAuthId
|
||||||
|
|
||||||
|
let
|
||||||
|
examDBTable = DBTable{..}
|
||||||
|
where
|
||||||
|
resultEExam = _dbrOutput . _1
|
||||||
|
resultSchool = _dbrOutput . _2
|
||||||
|
|
||||||
|
queryEExam = $(E.sqlIJproj 2 1)
|
||||||
|
querySchool = $(E.sqlIJproj 2 2)
|
||||||
|
|
||||||
|
dbtSQLQuery (eexam `E.InnerJoin` school) = do
|
||||||
|
E.on $ eexam E.^. ExternalExamSchool E.==. school E.^. SchoolId
|
||||||
|
let
|
||||||
|
isStaff
|
||||||
|
| Just authId <- mAuthId
|
||||||
|
= E.exists . E.from $ \eexamStaff ->
|
||||||
|
E.where_ $ eexamStaff E.^. ExternalExamStaffExam E.==. eexam E.^. ExternalExamId
|
||||||
|
E.&&. eexamStaff E.^. ExternalExamStaffUser E.==. E.val authId
|
||||||
|
| otherwise
|
||||||
|
= E.false
|
||||||
|
isStudent
|
||||||
|
| Just authId <- mAuthId
|
||||||
|
= E.exists . E.from $ \eexamResult ->
|
||||||
|
E.where_ $ eexamResult E.^. ExternalExamResultExam E.==. eexam E.^. ExternalExamId
|
||||||
|
E.&&. eexamResult E.^. ExternalExamResultUser E.==. E.val authId
|
||||||
|
| otherwise
|
||||||
|
= E.false
|
||||||
|
E.where_ $ isStaff E.||. isStudent
|
||||||
|
|
||||||
|
return (eexam, school)
|
||||||
|
dbtRowKey = queryEExam >>> (E.^. ExternalExamId)
|
||||||
|
dbtProj x@(view resultEExam -> Entity _ ExternalExam{..}) = do
|
||||||
|
guardM . hasReadAccessTo $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEShowR
|
||||||
|
return x
|
||||||
|
dbtColonnade = widgetColonnade $ mconcat
|
||||||
|
[ sortable (Just "term") (i18nCell MsgTerm) $ \(view resultEExam -> Entity _ ExternalExam{..}) -> i18nCell . ShortTermIdentifier $ unTermKey externalExamTerm
|
||||||
|
, sortable (Just "school") (i18nCell MsgSchool) $ \(view resultSchool -> Entity _ School{..}) -> i18nCell schoolName
|
||||||
|
, sortable (Just "course") (i18nCell MsgCourse) $ \(view resultEExam -> Entity _ ExternalExam{..}) -> i18nCell externalExamCourseName
|
||||||
|
, sortable (Just "name") (i18nCell MsgExamName) $ \(view resultEExam -> Entity _ ExternalExam{..}) -> anchorCell (EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEShowR) externalExamExamName
|
||||||
|
]
|
||||||
|
dbtSorting = Map.fromList
|
||||||
|
[ ("term", SortColumn $ queryEExam >>> (E.^. ExternalExamTerm))
|
||||||
|
, ("school", SortColumn $ querySchool >>> (E.^. SchoolName))
|
||||||
|
, ("course", SortColumn $ queryEExam >>> (E.^. ExternalExamCourseName))
|
||||||
|
, ("name", SortColumn $ queryEExam >>> (E.^. ExternalExamExamName))
|
||||||
|
]
|
||||||
|
dbtFilter = Map.empty
|
||||||
|
dbtFilterUI = const mempty
|
||||||
|
dbtStyle = def
|
||||||
|
dbtParams = def
|
||||||
|
dbtIdent :: Text
|
||||||
|
dbtIdent = "external-exams"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
examDBTableValidator = def
|
||||||
|
& defaultSorting [SortDescBy "term", SortAscBy "school", SortAscBy "course", SortAscBy "name"]
|
||||||
|
|
||||||
|
examTable <- runDB $ dbTableWidget' examDBTableValidator examDBTable
|
||||||
|
|
||||||
|
let heading = MsgMenuExternalExamList
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
examTable
|
||||||
72
src/Handler/ExternalExam/New.hs
Normal file
72
src/Handler/ExternalExam/New.hs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
module Handler.ExternalExam.New
|
||||||
|
( getEExamNewR, postEExamNewR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Jobs.Queue
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
|
||||||
|
import Handler.ExternalExam.StaffInvite
|
||||||
|
import Handler.ExternalExam.Form
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
|
||||||
|
getEExamNewR, postEExamNewR :: Handler Html
|
||||||
|
getEExamNewR = postEExamNewR
|
||||||
|
postEExamNewR = do
|
||||||
|
((newExamResult, newExamWidget'), newExamEnctype) <- runFormPost $ externalExamForm Nothing
|
||||||
|
|
||||||
|
formResult newExamResult $ \ExternalExamForm{..} -> do
|
||||||
|
insertRes <- runDBJobs $ do
|
||||||
|
insertRes <- insertUnique ExternalExam
|
||||||
|
{ externalExamTerm = eefTerm
|
||||||
|
, externalExamSchool = eefSchool
|
||||||
|
, externalExamCourseName = eefCourseName
|
||||||
|
, externalExamExamName = eefExamName
|
||||||
|
, externalExamDefaultTime = eefDefaultTime
|
||||||
|
, externalExamShowGrades = eefShowGrades
|
||||||
|
}
|
||||||
|
whenIsJust insertRes $ \eeId -> do
|
||||||
|
audit $ TransactionExternalExamEdit eeId
|
||||||
|
|
||||||
|
let eefOfficeSchools' = do
|
||||||
|
externalExamOfficeSchoolSchool <- Set.toList eefOfficeSchools
|
||||||
|
guard $ externalExamOfficeSchoolSchool /= eefSchool
|
||||||
|
let externalExamOfficeSchoolExam = eeId
|
||||||
|
return ExternalExamOfficeSchool{..}
|
||||||
|
insertMany_ eefOfficeSchools'
|
||||||
|
forM_ eefOfficeSchools' $ \ExternalExamOfficeSchool{..} ->
|
||||||
|
audit $ TransactionExternalExamOfficeSchoolEdit eeId externalExamOfficeSchoolSchool
|
||||||
|
|
||||||
|
let (invites, adds) = partitionEithers $ Set.toList eefStaff
|
||||||
|
eefStaff' = do
|
||||||
|
externalExamStaffUser <- adds
|
||||||
|
let externalExamStaffExam = eeId
|
||||||
|
return ExternalExamStaff{..}
|
||||||
|
insertMany_ eefStaff'
|
||||||
|
forM_ eefStaff' $ \ExternalExamStaff{..} ->
|
||||||
|
audit $ TransactionExternalExamStaffEdit eeId externalExamStaffUser
|
||||||
|
|
||||||
|
sinkInvitationsF externalExamStaffInvitationConfig $ map (, eeId, (InvDBDataExternalExamStaff, InvTokenDataExternalExamStaff)) invites
|
||||||
|
forM_ invites $ \invEmail ->
|
||||||
|
audit $ TransactionExternalExamStaffInviteEdit eeId invEmail
|
||||||
|
return insertRes
|
||||||
|
|
||||||
|
case insertRes of
|
||||||
|
Nothing -> addMessageI Error $ MsgExternalExamExists eefCourseName eefExamName
|
||||||
|
Just _ -> do
|
||||||
|
addMessageI Success $ MsgExternalExamCreated eefCourseName eefExamName
|
||||||
|
redirect $ EExamR eefTerm eefSchool eefCourseName eefExamName EEShowR
|
||||||
|
|
||||||
|
let heading = MsgMenuExternalExamNew
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
wrapForm newExamWidget' def
|
||||||
|
{ formAction = Just $ SomeRoute EExamNewR
|
||||||
|
, formEncoding = newExamEnctype
|
||||||
|
}
|
||||||
49
src/Handler/ExternalExam/Show.hs
Normal file
49
src/Handler/ExternalExam/Show.hs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
module Handler.ExternalExam.Show
|
||||||
|
( getEEShowR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
import Handler.ExternalExam.StaffInvite ()
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
|
||||||
|
getEEShowR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
|
||||||
|
getEEShowR tid ssh coursen examn = do
|
||||||
|
mUid <- maybeAuthId
|
||||||
|
|
||||||
|
(Entity _ ExternalExam{..}, fmap entityVal -> mResult, School{..}, staff, addSchools) <- runDB $ do
|
||||||
|
exam@(Entity eeId ExternalExam{..}) <- getBy404 $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
actualStaff <- E.select . E.from $ \(eeStaff `E.InnerJoin` user) -> do
|
||||||
|
E.on $ eeStaff E.^. ExternalExamStaffUser E.==. user E.^. UserId
|
||||||
|
E.where_ $ eeStaff E.^. ExternalExamStaffExam E.==. E.val eeId
|
||||||
|
E.orderBy [E.asc $ user E.^. UserDisplayName]
|
||||||
|
return user
|
||||||
|
maySeeInvites <- hasReadAccessTo $ EExamR tid ssh coursen examn EEGradesR
|
||||||
|
staffInvites <- if
|
||||||
|
| maySeeInvites -> sourceInvitationsF @ExternalExamStaff eeId
|
||||||
|
| otherwise -> return Map.empty
|
||||||
|
let staff = map Right actualStaff ++ map Left (Map.keys staffInvites)
|
||||||
|
addSchools <- E.select . E.from $ \(eeSchool `E.InnerJoin` school) -> do
|
||||||
|
E.on $ eeSchool E.^. ExternalExamOfficeSchoolSchool E.==. school E.^. SchoolId
|
||||||
|
E.where_ $ eeSchool E.^. ExternalExamOfficeSchoolExam E.==. E.val eeId
|
||||||
|
E.orderBy [E.asc $ school E.^. SchoolName]
|
||||||
|
return school
|
||||||
|
school <- getJust externalExamSchool
|
||||||
|
|
||||||
|
mResult <- fmap join . for mUid $ getBy . UniqueExternalExamResult eeId
|
||||||
|
|
||||||
|
return (exam, mResult, school, staff, addSchools)
|
||||||
|
|
||||||
|
let heading = CI.original examn
|
||||||
|
|
||||||
|
siteLayoutMsg heading $ do
|
||||||
|
setTitleI heading
|
||||||
|
|
||||||
|
$(widgetFile "external-exam-show")
|
||||||
79
src/Handler/ExternalExam/StaffInvite.hs
Normal file
79
src/Handler/ExternalExam/StaffInvite.hs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Handler.ExternalExam.StaffInvite
|
||||||
|
( externalExamStaffInvitationConfig
|
||||||
|
, getEEStaffInviteR, postEEStaffInviteR
|
||||||
|
, InvitableJunction(..), InvitationDBData(..), InvitationTokenData(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils.Invitations
|
||||||
|
|
||||||
|
import Text.Hamlet (ihamlet)
|
||||||
|
import Data.Aeson hiding (Result(..))
|
||||||
|
|
||||||
|
|
||||||
|
instance IsInvitableJunction ExternalExamStaff where
|
||||||
|
type InvitationFor ExternalExamStaff = ExternalExam
|
||||||
|
data InvitableJunction ExternalExamStaff = JunctionExternalExamStaff
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationDBData ExternalExamStaff = InvDBDataExternalExamStaff
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
data InvitationTokenData ExternalExamStaff = InvTokenDataExternalExamStaff
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
_InvitableJunction = iso
|
||||||
|
(\ExternalExamStaff{..} -> (externalExamStaffUser, externalExamStaffExam, JunctionExternalExamStaff))
|
||||||
|
(\(externalExamStaffUser, externalExamStaffExam, JunctionExternalExamStaff{}) -> ExternalExamStaff{..})
|
||||||
|
|
||||||
|
instance ToJSON (InvitableJunction ExternalExamStaff) where
|
||||||
|
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 1 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 1 }
|
||||||
|
instance FromJSON (InvitableJunction ExternalExamStaff) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 1 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationDBData ExternalExamStaff) where
|
||||||
|
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
instance FromJSON (InvitationDBData ExternalExamStaff) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
|
||||||
|
instance ToJSON (InvitationTokenData ExternalExamStaff) where
|
||||||
|
toJSON = genericToJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
toEncoding = genericToEncoding defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
instance FromJSON (InvitationTokenData ExternalExamStaff) where
|
||||||
|
parseJSON = genericParseJSON defaultOptions { constructorTagModifier = camelToPathPiece' 3 }
|
||||||
|
|
||||||
|
externalExamStaffInvitationConfig :: InvitationConfig ExternalExamStaff
|
||||||
|
externalExamStaffInvitationConfig = InvitationConfig{..}
|
||||||
|
where
|
||||||
|
invitationRoute (Entity _ ExternalExam{..}) _ = return $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEStaffInviteR
|
||||||
|
invitationResolveFor _ = do
|
||||||
|
cRoute <- getCurrentRoute
|
||||||
|
case cRoute of
|
||||||
|
Just (EExamR tid ssh coursen examn EEStaffInviteR) ->
|
||||||
|
getKeyBy404 $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
_other -> error "externalExamStaffInvitationConfig called from unsupported route"
|
||||||
|
invitationSubject (Entity _ ExternalExam{..}) _ = return . SomeMessage $ MsgMailSubjectExternalExamStaffInvitation externalExamCourseName externalExamExamName
|
||||||
|
invitationHeading (Entity _ ExternalExam{..}) _ = return . SomeMessage $ MsgExternalExamStaffInviteHeading externalExamCourseName externalExamExamName
|
||||||
|
invitationExplanation _ _ = return [ihamlet|_{SomeMessage MsgExternalExamStaffInviteExplanation}|]
|
||||||
|
invitationTokenConfig _ _ = do
|
||||||
|
itAuthority <- Right <$> liftHandler requireAuthId
|
||||||
|
return $ InvitationTokenConfig itAuthority Nothing (Just Nothing) Nothing
|
||||||
|
invitationRestriction _ _ = return Authorized
|
||||||
|
invitationForm _ (InvDBDataExternalExamStaff, _) _ = pure (JunctionExternalExamStaff, ())
|
||||||
|
invitationInsertHook invEmail _ _ ExternalExamStaff{..} _ act = do
|
||||||
|
res <- act
|
||||||
|
|
||||||
|
audit $ TransactionExternalExamStaffInviteDelete externalExamStaffExam invEmail
|
||||||
|
audit $ TransactionExternalExamStaffEdit externalExamStaffExam externalExamStaffUser
|
||||||
|
return res
|
||||||
|
invitationSuccessMsg (Entity _ ExternalExam{..}) (Entity _ ExternalExamStaff{})
|
||||||
|
= return . SomeMessage $ MsgExternalExamStaffInvitationAccepted externalExamCourseName externalExamExamName
|
||||||
|
invitationUltDest (Entity _ ExternalExam{..}) _ = return . SomeRoute $ EExamR externalExamTerm externalExamSchool externalExamCourseName externalExamExamName EEShowR
|
||||||
|
|
||||||
|
|
||||||
|
getEEStaffInviteR, postEEStaffInviteR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
|
||||||
|
getEEStaffInviteR = postEEStaffInviteR
|
||||||
|
postEEStaffInviteR = invitationR externalExamStaffInvitationConfig
|
||||||
18
src/Handler/ExternalExam/Users.hs
Normal file
18
src/Handler/ExternalExam/Users.hs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module Handler.ExternalExam.Users
|
||||||
|
( getEEUsersR, postEEUsersR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Handler.Utils.ExternalExam.Users
|
||||||
|
|
||||||
|
getEEUsersR, postEEUsersR :: TermId -> SchoolId -> CourseName -> ExamName -> Handler Html
|
||||||
|
getEEUsersR = postEEUsersR
|
||||||
|
postEEUsersR tid ssh coursen examn = do
|
||||||
|
(_, table) <- runDB $ do
|
||||||
|
eExam <- getBy404 $ UniqueExternalExam tid ssh coursen examn
|
||||||
|
makeExternalExamUsersTable EEUMUsers eExam
|
||||||
|
|
||||||
|
siteLayoutMsg (MsgExternalExamUsers coursen examn) $ do
|
||||||
|
setTitleI MsgBreadcrumbExternalExamUsers
|
||||||
|
table
|
||||||
79
src/Handler/Participants.hs
Normal file
79
src/Handler/Participants.hs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||||
|
module Handler.Participants
|
||||||
|
( getParticipantsListR
|
||||||
|
, getParticipantsR
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Handler.Utils.Csv
|
||||||
|
import Handler.Utils.ContentDisposition
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
|
||||||
|
data ParticipantEntry = ParticipantEntry
|
||||||
|
{ peCourse :: CourseName
|
||||||
|
, peEmail :: UserEmail
|
||||||
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
instance ToNamedRecord ParticipantEntry where
|
||||||
|
toNamedRecord ParticipantEntry{..} = Csv.namedRecord
|
||||||
|
[ "course" Csv..= peCourse
|
||||||
|
, "email" Csv..= peEmail
|
||||||
|
]
|
||||||
|
|
||||||
|
instance DefaultOrdered ParticipantEntry where
|
||||||
|
headerOrder _ = Csv.header ["course", "email"]
|
||||||
|
|
||||||
|
|
||||||
|
getParticipantsListR :: Handler Html
|
||||||
|
getParticipantsListR = do
|
||||||
|
schoolTerms'' <- runDB . E.select . E.from $ \(school `E.InnerJoin` term) -> do
|
||||||
|
E.on E.true
|
||||||
|
|
||||||
|
E.where_ . E.exists . E.from $ \(course `E.InnerJoin` participant) -> do
|
||||||
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
||||||
|
E.where_ $ course E.^. CourseTerm E.==. term E.^. TermId
|
||||||
|
E.&&. course E.^. CourseSchool E.==. school E.^. SchoolId
|
||||||
|
|
||||||
|
return (school E.^. SchoolId, term E.^. TermId)
|
||||||
|
|
||||||
|
schoolTerms' <- flip filterM schoolTerms'' $ \(E.Value ssh, E.Value tid) ->
|
||||||
|
hasReadAccessTo $ ParticipantsR tid ssh
|
||||||
|
|
||||||
|
let schoolTerms :: Set (SchoolId, TermId)
|
||||||
|
schoolTerms = setOf (folded . $(multifocusG 2) (_1 . _Value) (_2 . _Value)) schoolTerms'
|
||||||
|
|
||||||
|
siteLayoutMsg MsgMenuParticipantsList $ do
|
||||||
|
setTitleI MsgMenuParticipantsList
|
||||||
|
|
||||||
|
let schools :: Set SchoolId
|
||||||
|
schools = Set.map (view _1) schoolTerms
|
||||||
|
terms :: Set TermId
|
||||||
|
terms = Set.map (view _2) schoolTerms
|
||||||
|
$(widgetFile "participants-list")
|
||||||
|
|
||||||
|
getParticipantsR :: TermId -> SchoolId -> Handler TypedContent
|
||||||
|
getParticipantsR tid ssh = do
|
||||||
|
csvName <- timestampCsv <*> fmap ((flip addExtension `on` unpack) extensionCsv) (getMessageRender <*> pure (MsgParticipantsCsvName tid ssh))
|
||||||
|
setContentDisposition' $ Just csvName
|
||||||
|
respondDefaultOrderedCsvDB $ E.selectSource partQuery .| C.map toParticipantEntry
|
||||||
|
where
|
||||||
|
partQuery = E.from $ \(course `E.InnerJoin` participant `E.InnerJoin` user) -> do
|
||||||
|
E.on $ user E.^. UserId E.==. participant E.^. CourseParticipantUser
|
||||||
|
E.on $ course E.^. CourseId E.==. participant E.^. CourseParticipantCourse
|
||||||
|
|
||||||
|
E.where_ $ course E.^. CourseTerm E.==. E.val tid
|
||||||
|
E.&&. course E.^. CourseSchool E.==. E.val ssh
|
||||||
|
|
||||||
|
return (course E.^. CourseName, user E.^. UserEmail)
|
||||||
|
|
||||||
|
toParticipantEntry (E.Value peCourse, E.Value peEmail) = ParticipantEntry{..}
|
||||||
@ -892,7 +892,7 @@ correctorInvitationConfig = InvitationConfig{..}
|
|||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
|
invitationForm _ (InvDBDataSheetCorrector cLoad cState, _) _ = pure $ (JunctionSheetCorrector cLoad cState, ())
|
||||||
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
|
||||||
Course{..} <- get404 sheetCourse
|
Course{..} <- get404 sheetCourse
|
||||||
|
|||||||
@ -114,7 +114,7 @@ submissionUserInvitationConfig = InvitationConfig{..}
|
|||||||
return InvitationTokenConfig{..}
|
return InvitationTokenConfig{..}
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionSubmissionUser, ())
|
invitationForm _ _ _ = pure (JunctionSubmissionUser, ())
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ Submission{..}) _ = do
|
invitationSuccessMsg (Entity _ Submission{..}) _ = do
|
||||||
Sheet{..} <- getJust submissionSheet
|
Sheet{..} <- getJust submissionSheet
|
||||||
return . SomeMessage $ MsgSubmissionUserInvitationAccepted sheetName
|
return . SomeMessage $ MsgSubmissionUserInvitationAccepted sheetName
|
||||||
|
|||||||
@ -68,7 +68,7 @@ tutorInvitationConfig = InvitationConfig{..}
|
|||||||
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
return $ InvitationTokenConfig itAuthority Nothing Nothing Nothing
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ _ _ = pure (JunctionTutor, ())
|
invitationForm _ _ _ = pure (JunctionTutor, ())
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted tutorialName
|
invitationSuccessMsg (Entity _ Tutorial{..}) _ = return . SomeMessage $ MsgCorrectorInvitationAccepted tutorialName
|
||||||
invitationUltDest (Entity _ Tutorial{..}) _ = do
|
invitationUltDest (Entity _ Tutorial{..}) _ = do
|
||||||
Course{..} <- get404 tutorialCourse
|
Course{..} <- get404 tutorialCourse
|
||||||
|
|||||||
@ -579,7 +579,7 @@ functionInvitationConfig = InvitationConfig{..}
|
|||||||
return InvitationTokenConfig{..}
|
return InvitationTokenConfig{..}
|
||||||
invitationRestriction _ _ = return Authorized
|
invitationRestriction _ _ = return Authorized
|
||||||
invitationForm _ (_, InvTokenDataUserFunction{..}) _ = pure $ (JunctionUserFunction invTokenUserFunctionFunction, ())
|
invitationForm _ (_, InvTokenDataUserFunction{..}) _ = pure $ (JunctionUserFunction invTokenUserFunctionFunction, ())
|
||||||
invitationInsertHook _ _ _ _ = id
|
invitationInsertHook _ _ _ _ _ = id
|
||||||
invitationSuccessMsg (Entity _ School{..}) (Entity _ UserFunction{..}) = do
|
invitationSuccessMsg (Entity _ School{..}) (Entity _ UserFunction{..}) = do
|
||||||
MsgRenderer mr <- getMsgRenderer
|
MsgRenderer mr <- getMsgRenderer
|
||||||
return . SomeMessage . MsgSchoolFunctionInvitationAccepted schoolName $ mr userFunctionFunction
|
return . SomeMessage . MsgSchoolFunctionInvitationAccepted schoolName $ mr userFunctionFunction
|
||||||
|
|||||||
61
src/Handler/Utils/ExamOffice/ExternalExam.hs
Normal file
61
src/Handler/Utils/ExamOffice/ExternalExam.hs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
module Handler.Utils.ExamOffice.ExternalExam
|
||||||
|
( resultIsSynced
|
||||||
|
, examOfficeExternalExamResultAuth
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import.NoFoundation
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
|
||||||
|
resultIsSynced :: E.SqlExpr (E.Value UserId) -- ^ office
|
||||||
|
-> E.SqlExpr (Entity ExternalExamResult)
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
|
resultIsSynced authId eexamResult = (hasSchool E.&&. allSchools) E.||. (E.not_ hasSchool E.&&. anySync)
|
||||||
|
where
|
||||||
|
anySync = E.exists . E.from $ \synced ->
|
||||||
|
E.where_ $ synced E.^. ExamOfficeExternalResultSyncedResult E.==. eexamResult E.^. ExternalExamResultId
|
||||||
|
E.&&. synced E.^. ExamOfficeExternalResultSyncedTime E.>=. eexamResult E.^. ExternalExamResultLastChanged
|
||||||
|
|
||||||
|
hasSchool = E.exists . E.from $ \userFunction ->
|
||||||
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
|
||||||
|
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
|
||||||
|
allSchools = E.not_ . E.exists . E.from $ \userFunction -> do
|
||||||
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
|
||||||
|
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
|
||||||
|
E.where_ . E.not_ . E.exists . E.from $ \synced ->
|
||||||
|
E.where_ $ synced E.^. ExamOfficeExternalResultSyncedSchool E.==. E.just (userFunction E.^. UserFunctionSchool)
|
||||||
|
E.&&. synced E.^. ExamOfficeExternalResultSyncedResult E.==. eexamResult E.^. ExternalExamResultId
|
||||||
|
E.&&. synced E.^. ExamOfficeExternalResultSyncedTime E.>=. eexamResult E.^. ExternalExamResultLastChanged
|
||||||
|
|
||||||
|
|
||||||
|
examOfficeExternalExamResultAuth :: E.SqlExpr (E.Value UserId) -- ^ office
|
||||||
|
-> E.SqlExpr (Entity ExternalExamResult)
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
|
examOfficeExternalExamResultAuth authId eexamResult = authByUser E.||. authByField E.||. authBySchool E.||. authByExtraSchool
|
||||||
|
where
|
||||||
|
authByField = E.exists . E.from $ \(examOfficeField `E.InnerJoin` studyFeatures) -> do
|
||||||
|
E.on $ studyFeatures E.^. StudyFeaturesField E.==. examOfficeField E.^. ExamOfficeFieldField
|
||||||
|
E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. eexamResult E.^. ExternalExamResultUser
|
||||||
|
E.&&. examOfficeField E.^. ExamOfficeFieldOffice E.==. authId
|
||||||
|
E.&&. examOfficeField E.^. ExamOfficeFieldField E.==. studyFeatures E.^. StudyFeaturesField
|
||||||
|
E.where_ $ examOfficeField E.^. ExamOfficeFieldForced
|
||||||
|
E.||. E.exists (E.from $ \userFunction ->
|
||||||
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
|
||||||
|
E.&&. userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
|
||||||
|
)
|
||||||
|
|
||||||
|
authByUser = E.exists . E.from $ \examOfficeUser ->
|
||||||
|
E.where_ $ examOfficeUser E.^. ExamOfficeUserOffice E.==. authId
|
||||||
|
E.&&. examOfficeUser E.^. ExamOfficeUserUser E.==. eexamResult E.^. ExternalExamResultUser
|
||||||
|
|
||||||
|
authBySchool = E.exists . E.from $ \(userFunction `E.InnerJoin` eexam) -> do
|
||||||
|
E.on $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
|
||||||
|
E.&&. userFunction E.^. UserFunctionSchool E.==. eexam E.^. ExternalExamSchool
|
||||||
|
E.where_ $ eexam E.^. ExternalExamId E.==. eexamResult E.^. ExternalExamResultExam
|
||||||
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
|
||||||
|
authByExtraSchool = E.exists . E.from $ \(userFunction `E.InnerJoin` eexamSchool) -> do
|
||||||
|
E.on $ userFunction E.^. UserFunctionFunction E.==. E.val SchoolExamOffice
|
||||||
|
E.&&. userFunction E.^. UserFunctionSchool E.==. eexamSchool E.^. ExternalExamOfficeSchoolSchool
|
||||||
|
E.where_ $ eexamSchool E.^. ExternalExamOfficeSchoolExam E.==. eexamResult E.^. ExternalExamResultExam
|
||||||
|
E.where_ $ userFunction E.^. UserFunctionUser E.==. authId
|
||||||
476
src/Handler/Utils/ExternalExam/Users.hs
Normal file
476
src/Handler/Utils/ExternalExam/Users.hs
Normal file
@ -0,0 +1,476 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-redundant-constraints #-}
|
||||||
|
|
||||||
|
module Handler.Utils.ExternalExam.Users where
|
||||||
|
|
||||||
|
import Import hiding ((.:))
|
||||||
|
|
||||||
|
import Handler.Utils
|
||||||
|
import Handler.Utils.Csv
|
||||||
|
import Handler.Utils.Users
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import qualified Colonnade
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
import Data.Csv ((.:))
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import qualified Handler.Utils.ExamOffice.ExternalExam as ExternalExam
|
||||||
|
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.Text.Lens as Text
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
|
||||||
|
data ExternalExamUserMode = EEUMUsers | EEUMGrades
|
||||||
|
deriving (Eq, Ord, Read, Show, Bounded, Enum, Generic, Typeable)
|
||||||
|
instance Universe ExternalExamUserMode
|
||||||
|
instance Finite ExternalExamUserMode
|
||||||
|
nullaryPathPiece ''ExternalExamUserMode $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
|
||||||
|
type ExternalExamUserTableExpr = E.SqlExpr (Entity ExternalExamResult)
|
||||||
|
`E.InnerJoin` E.SqlExpr (Entity User)
|
||||||
|
|
||||||
|
type ExternalExamUserTableData = DBRow ( Entity ExternalExamResult
|
||||||
|
, Entity User
|
||||||
|
, Bool
|
||||||
|
, [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)]
|
||||||
|
)
|
||||||
|
|
||||||
|
queryUser :: Getter ExternalExamUserTableExpr (E.SqlExpr (Entity User))
|
||||||
|
queryUser = to $(E.sqlIJproj 2 2)
|
||||||
|
|
||||||
|
queryResult :: Getter ExternalExamUserTableExpr (E.SqlExpr (Entity ExternalExamResult))
|
||||||
|
queryResult = to $(E.sqlIJproj 2 1)
|
||||||
|
|
||||||
|
queryIsSynced :: E.SqlExpr (E.Value UserId) -> Getter ExternalExamUserTableExpr (E.SqlExpr (E.Value Bool))
|
||||||
|
queryIsSynced authId = to $ ExternalExam.resultIsSynced authId <$> view queryResult
|
||||||
|
|
||||||
|
resultUser :: Lens' ExternalExamUserTableData (Entity User)
|
||||||
|
resultUser = _dbrOutput . _2
|
||||||
|
|
||||||
|
resultResult :: Lens' ExternalExamUserTableData (Entity ExternalExamResult)
|
||||||
|
resultResult = _dbrOutput . _1
|
||||||
|
|
||||||
|
resultIsSynced :: Lens' ExternalExamUserTableData Bool
|
||||||
|
resultIsSynced = _dbrOutput . _3
|
||||||
|
|
||||||
|
resultSynchronised :: Traversal' ExternalExamUserTableData (UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)
|
||||||
|
resultSynchronised = _dbrOutput . _4 . traverse
|
||||||
|
|
||||||
|
|
||||||
|
data ExternalExamUserTableCsv = ExternalExamUserTableCsv
|
||||||
|
{ csvEUserSurname :: Maybe Text
|
||||||
|
, csvEUserFirstName :: Maybe Text
|
||||||
|
, csvEUserName :: Maybe Text
|
||||||
|
, csvEUserMatriculation :: Maybe Text
|
||||||
|
, csvEUserOccurrenceStart :: ZonedTime
|
||||||
|
, csvEUserExamResult :: ExamResultPassedGrade
|
||||||
|
} deriving (Generic)
|
||||||
|
makeLenses_ ''ExternalExamUserTableCsv
|
||||||
|
|
||||||
|
externalExamUserTableCsvOptions :: Csv.Options
|
||||||
|
externalExamUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 3 }
|
||||||
|
|
||||||
|
instance ToNamedRecord ExternalExamUserTableCsv where
|
||||||
|
toNamedRecord = Csv.genericToNamedRecord externalExamUserTableCsvOptions
|
||||||
|
|
||||||
|
instance DefaultOrdered ExternalExamUserTableCsv where
|
||||||
|
headerOrder = Csv.genericHeaderOrder externalExamUserTableCsvOptions
|
||||||
|
|
||||||
|
instance FromNamedRecord ExternalExamUserTableCsv where
|
||||||
|
parseNamedRecord csv
|
||||||
|
= ExternalExamUserTableCsv
|
||||||
|
<$> csv .:?? "surname"
|
||||||
|
<*> csv .:?? "first-name"
|
||||||
|
<*> csv .:?? "name"
|
||||||
|
<*> csv .:?? "matriculation"
|
||||||
|
<*> csv .: "occurrence-start"
|
||||||
|
<*> csv .: "exam-result"
|
||||||
|
|
||||||
|
|
||||||
|
instance CsvColumnsExplained ExternalExamUserTableCsv where
|
||||||
|
csvColumnsExplanations = genericCsvColumnsExplanations externalExamUserTableCsvOptions $ Map.fromList
|
||||||
|
[ ('csvEUserSurname , MsgCsvColumnExamUserSurname )
|
||||||
|
, ('csvEUserFirstName , MsgCsvColumnExamUserFirstName )
|
||||||
|
, ('csvEUserName , MsgCsvColumnExamUserName )
|
||||||
|
, ('csvEUserMatriculation , MsgCsvColumnExamUserMatriculation )
|
||||||
|
, ('csvEUserOccurrenceStart , MsgCsvColumnExamOfficeExamUserOccurrenceStart )
|
||||||
|
, ('csvEUserExamResult , MsgCsvColumnExamUserResult )
|
||||||
|
]
|
||||||
|
|
||||||
|
data ExternalExamUserAction
|
||||||
|
= ExternalExamUserMarkSynchronised
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ExternalExamUserAction
|
||||||
|
instance Finite ExternalExamUserAction
|
||||||
|
nullaryPathPiece ''ExternalExamUserAction $ camelToPathPiece' 3
|
||||||
|
embedRenderMessage ''UniWorX ''ExternalExamUserAction id
|
||||||
|
|
||||||
|
data ExternalExamUserActionData
|
||||||
|
= ExternalExamUserMarkSynchronisedData
|
||||||
|
|
||||||
|
newtype ExternalExamUserCsvExportDataGrades = ExternalExamUserCsvExportDataGrades
|
||||||
|
{ csvEEUserMarkSynchronised :: Bool
|
||||||
|
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
|
||||||
|
|
||||||
|
data ExamUserCsvException
|
||||||
|
= ExamUserCsvExceptionNoMatchingUser
|
||||||
|
deriving (Show, Generic, Typeable)
|
||||||
|
|
||||||
|
instance Exception ExamUserCsvException
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ExamUserCsvException id
|
||||||
|
|
||||||
|
|
||||||
|
data ExternalExamUserCsvActionClass
|
||||||
|
= ExternalExamUserCsvRegister
|
||||||
|
| ExternalExamUserCsvDeregister
|
||||||
|
| ExternalExamUserCsvSetTime
|
||||||
|
| ExternalExamUserCsvSetResult
|
||||||
|
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||||
|
embedRenderMessage ''UniWorX ''ExternalExamUserCsvActionClass id
|
||||||
|
|
||||||
|
data ExternalExamUserCsvAction
|
||||||
|
= ExternalExamUserCsvRegisterData
|
||||||
|
{ externalExamUserCsvActUser :: UserId
|
||||||
|
, externalExamUserCsvActTime :: UTCTime
|
||||||
|
, externalExamUserCsvActResult :: ExamResultPassedGrade
|
||||||
|
}
|
||||||
|
| ExternalExamUserCsvSetTimeData
|
||||||
|
{ externalExamUserCsvActRegistration :: ExternalExamResultId
|
||||||
|
, externalExamUserCsvActTime :: UTCTime
|
||||||
|
}
|
||||||
|
| ExternalExamUserCsvSetResultData
|
||||||
|
{ externalExamUserCsvActRegistration :: ExternalExamResultId
|
||||||
|
, externalExamUserCsvActResult :: ExamResultPassedGrade
|
||||||
|
}
|
||||||
|
| ExternalExamUserCsvDeregisterData
|
||||||
|
{ externalExamUserCsvActRegistration :: ExternalExamResultId
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
deriveJSON defaultOptions
|
||||||
|
{ constructorTagModifier = over Text.packed $ Text.intercalate "-" . map Text.toLower . drop 4 . dropEnd 1 . splitCamel
|
||||||
|
, fieldLabelModifier = camelToPathPiece' 5
|
||||||
|
, sumEncoding = TaggedObject "action" "data"
|
||||||
|
} ''ExternalExamUserCsvAction
|
||||||
|
|
||||||
|
|
||||||
|
makeExternalExamUsersTable :: ExternalExamUserMode
|
||||||
|
-> Entity ExternalExam
|
||||||
|
-> DB (FormResult (ExternalExamUserActionData, Set ExternalExamResultId), Widget)
|
||||||
|
makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
|
||||||
|
let tid = externalExamTerm
|
||||||
|
ssh = externalExamSchool
|
||||||
|
coursen = externalExamCourseName
|
||||||
|
examn = externalExamExamName
|
||||||
|
|
||||||
|
uid <- requireAuthId
|
||||||
|
csvName <- getMessageRender <*> pure (MsgExternalExamUserCsvName tid ssh coursen examn)
|
||||||
|
isLecturer <- hasReadAccessTo $ EExamR tid ssh coursen examn EEUsersR
|
||||||
|
currentRoute <- fromMaybe (error "makeExternalExamUsersTable called from 404-handler") <$> getCurrentRoute
|
||||||
|
|
||||||
|
let
|
||||||
|
resultView :: ExamResultGrade -> ExamResultPassedGrade
|
||||||
|
resultView = fmap $ bool (Left . view passingGrade) Right externalExamShowGrades
|
||||||
|
|
||||||
|
dbtSQLQuery = runReaderT $ do
|
||||||
|
result <- view queryResult
|
||||||
|
user <- view queryUser
|
||||||
|
isSynced <- view . queryIsSynced $ E.val uid
|
||||||
|
|
||||||
|
lift $ do
|
||||||
|
E.on $ result E.^. ExternalExamResultUser E.==. user E.^. UserId
|
||||||
|
|
||||||
|
E.where_ $ result E.^. ExternalExamResultExam E.==. E.val eeId
|
||||||
|
|
||||||
|
unless (isLecturer || mode == EEUMUsers) $
|
||||||
|
E.where_ $ ExternalExam.examOfficeExternalExamResultAuth (E.val uid) result
|
||||||
|
|
||||||
|
return (result, user, isSynced)
|
||||||
|
dbtRowKey = views queryResult (E.^. ExternalExamResultId)
|
||||||
|
|
||||||
|
dbtProj :: DBRow _ -> MaybeT (YesodDB UniWorX) ExternalExamUserTableData
|
||||||
|
dbtProj = runReaderT $ (asks . set _dbrOutput) <=< magnify _dbrOutput $
|
||||||
|
(,,,)
|
||||||
|
<$> view _1 <*> view _2 <*> view (_3 . _Value)
|
||||||
|
<*> getSynchronised
|
||||||
|
where
|
||||||
|
getSynchronised :: ReaderT _ (MaybeT (YesodDB UniWorX)) [(UserDisplayName, UserSurname, UTCTime, Set SchoolShorthand)]
|
||||||
|
getSynchronised = do
|
||||||
|
resId <- view $ _1 . _entityKey
|
||||||
|
syncs <- lift . lift . E.select . E.from $ \(examOfficeExternalResultSynced `E.InnerJoin` user) -> do
|
||||||
|
E.on $ examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedOffice E.==. user E.^. UserId
|
||||||
|
E.where_ $ examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedResult E.==. E.val resId
|
||||||
|
return ( examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedOffice
|
||||||
|
, ( user E.^. UserDisplayName
|
||||||
|
, user E.^. UserSurname
|
||||||
|
, examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedTime
|
||||||
|
, examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedSchool
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let syncs' = Map.fromListWith
|
||||||
|
(\(dn, sn, t, sshs) (_, _, _, sshs') -> (dn, sn, t, Set.union sshs sshs'))
|
||||||
|
[ ((officeId, t), (dn, sn, t, maybe Set.empty Set.singleton ssh'))
|
||||||
|
| (E.Value officeId, (E.Value dn, E.Value sn, E.Value t, fmap unSchoolKey . E.unValue -> ssh')) <- syncs
|
||||||
|
]
|
||||||
|
return $ Map.elems syncs'
|
||||||
|
|
||||||
|
colSynced = Colonnade.singleton (fromSortable . Sortable (Just "is-synced") $ i18nCell MsgExamUserSynchronised) $ \x -> cell . flip runReaderT x $ do
|
||||||
|
syncs <- asks $ sortOn (Down . view _3) . toListOf resultSynchronised
|
||||||
|
lastChange <- view $ resultResult . _entityVal . _externalExamResultLastChanged
|
||||||
|
user <- view $ resultUser . _entityVal
|
||||||
|
isSynced <- view resultIsSynced
|
||||||
|
let
|
||||||
|
hasSyncs = has folded syncs
|
||||||
|
|
||||||
|
syncs' = [ Right sync | sync@(_, _, t, _) <- syncs, t > lastChange]
|
||||||
|
++ [ Left lastChange ]
|
||||||
|
++ [ Right sync | sync@(_, _, t, _) <- syncs, t <= lastChange]
|
||||||
|
|
||||||
|
syncIcon :: Widget
|
||||||
|
syncIcon
|
||||||
|
| not isSynced
|
||||||
|
, not hasSyncs
|
||||||
|
= mempty
|
||||||
|
| not isSynced
|
||||||
|
= toWidget iconNotOK
|
||||||
|
| otherwise
|
||||||
|
= toWidget iconOK
|
||||||
|
|
||||||
|
syncsModal :: Widget
|
||||||
|
syncsModal = $(widgetFile "exam-office/exam-result-synced")
|
||||||
|
lift $ bool id (flip modal $ Right syncsModal) hasSyncs syncIcon
|
||||||
|
|
||||||
|
dbtColonnade :: Colonnade Sortable _ _
|
||||||
|
dbtColonnade = mconcat
|
||||||
|
[ case mode of
|
||||||
|
EEUMGrades -> mconcat
|
||||||
|
[ dbSelect (applying _2) id $ return . view (resultResult . _entityKey)
|
||||||
|
, colSynced
|
||||||
|
]
|
||||||
|
_other -> mempty
|
||||||
|
, colUserDisplayName (resultUser . _entityVal . $(multifocusL 2) _userDisplayName _userSurname)
|
||||||
|
, colUserMatriculation (resultUser . _entityVal . _userMatrikelnummer)
|
||||||
|
, Colonnade.singleton (fromSortable . Sortable (Just "occurrence-start") $ i18nCell MsgExamTime) $ \x -> cell . flip runReaderT x $ do
|
||||||
|
t <- view $ resultResult . _entityVal . _externalExamResultTime
|
||||||
|
lift $ formatTimeW SelFormatDateTime t
|
||||||
|
, colExamResult externalExamShowGrades (resultResult . _entityVal . _externalExamResultResult)
|
||||||
|
]
|
||||||
|
dbtSorting = mconcat
|
||||||
|
[ sortUserName' (queryUser . to ((,) <$> (E.^. UserDisplayName) <*> (E.^. UserSurname)))
|
||||||
|
, sortUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
|
||||||
|
, sortOccurrenceStart (queryResult . to (E.^. ExternalExamResultTime))
|
||||||
|
, maybeOpticSortColumn (sortExamResult externalExamShowGrades) (queryResult . to (E.^. ExternalExamResultResult))
|
||||||
|
, singletonMap "is-synced" . SortColumn $ view (queryIsSynced $ E.val uid)
|
||||||
|
]
|
||||||
|
dbtFilter = mconcat
|
||||||
|
[ fltrUserName' (queryUser . to (E.^. UserDisplayName))
|
||||||
|
, fltrUserMatriculation (queryUser . to (E.^. UserMatrikelnummer))
|
||||||
|
, fltrExamResultPoints externalExamShowGrades (queryResult . to (E.^. ExternalExamResultResult))
|
||||||
|
, singletonMap "is-synced" . FilterColumn $ E.mkExactFilter (view . queryIsSynced $ E.val uid)
|
||||||
|
]
|
||||||
|
dbtFilterUI = mconcat
|
||||||
|
[ fltrUserNameUI'
|
||||||
|
, fltrUserMatriculationUI
|
||||||
|
, fltrExamResultPointsUI externalExamShowGrades
|
||||||
|
, case mode of
|
||||||
|
EEUMGrades ->
|
||||||
|
\mPrev -> prismAForm (singletonFilter "is-synced" . maybePrism _PathPiece) mPrev $ aopt (boolField . Just $ SomeMessage MsgBoolIrrelevant) (fslI MsgExamUserSynchronised)
|
||||||
|
_other -> mempty
|
||||||
|
]
|
||||||
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
|
dbtParams = DBParamsForm
|
||||||
|
{ dbParamsFormMethod = POST
|
||||||
|
, dbParamsFormAction = Just $ SomeRoute currentRoute
|
||||||
|
, dbParamsFormAttrs = []
|
||||||
|
, dbParamsFormSubmit = case mode of
|
||||||
|
EEUMGrades -> FormSubmit
|
||||||
|
_other -> FormNoSubmit
|
||||||
|
, dbParamsFormAdditional = case mode of
|
||||||
|
EEUMGrades -> \csrf -> do
|
||||||
|
let
|
||||||
|
actionMap :: Map ExternalExamUserAction (AForm Handler ExternalExamUserActionData)
|
||||||
|
actionMap = Map.fromList
|
||||||
|
[ ( ExternalExamUserMarkSynchronised
|
||||||
|
, pure ExternalExamUserMarkSynchronisedData
|
||||||
|
)
|
||||||
|
]
|
||||||
|
(res, formWgt) <- multiActionM actionMap (fslI MsgAction) Nothing csrf
|
||||||
|
let formRes = (, mempty) . First . Just <$> res
|
||||||
|
return (formRes, formWgt)
|
||||||
|
_other -> \csrf -> return (FormMissing, toWidget csrf)
|
||||||
|
, dbParamsFormEvaluate = liftHandler . runFormPost
|
||||||
|
, dbParamsFormResult = id
|
||||||
|
, dbParamsFormIdent = def
|
||||||
|
}
|
||||||
|
dbtIdent = mode
|
||||||
|
dbtCsvEncode = case mode of
|
||||||
|
EEUMGrades -> Just DBTCsvEncode
|
||||||
|
{ dbtCsvExportForm = ExternalExamUserCsvExportDataGrades
|
||||||
|
<$> apopt checkBoxField (fslI MsgExamUserMarkSynchronisedCsv & setTooltip MsgExamUserMarkSynchronisedCsvTip) (Just False)
|
||||||
|
, dbtCsvDoEncode = \ExternalExamUserCsvExportDataGrades{..} -> C.mapM $ \(E.Value k, row) -> do
|
||||||
|
when csvEEUserMarkSynchronised $ externalExamResultMarkSynchronised k
|
||||||
|
return $ encodeCsv' row
|
||||||
|
, dbtCsvName = unpack csvName
|
||||||
|
, dbtCsvNoExportData = Nothing
|
||||||
|
, dbtCsvHeader = const . return $ headerOrder (error "headerOrder" :: ExternalExamUserTableCsv)
|
||||||
|
}
|
||||||
|
EEUMUsers -> simpleCsvEncode csvName encodeCsv'
|
||||||
|
where
|
||||||
|
encodeCsv' :: ExternalExamUserTableData -> ExternalExamUserTableCsv
|
||||||
|
encodeCsv' row = ExternalExamUserTableCsv
|
||||||
|
{ csvEUserSurname = row ^? resultUser . _entityVal . _userSurname
|
||||||
|
, csvEUserFirstName = row ^? resultUser . _entityVal . _userFirstName
|
||||||
|
, csvEUserName = row ^? resultUser . _entityVal . _userDisplayName
|
||||||
|
, csvEUserMatriculation = row ^? resultUser . _entityVal . _userMatrikelnummer . _Just
|
||||||
|
, csvEUserOccurrenceStart = row ^. resultResult . _entityVal . _externalExamResultTime . to utcToZonedTime
|
||||||
|
, csvEUserExamResult = row ^. resultResult . _entityVal . _externalExamResultResult . to (fmap $ bool (Left . view passingGrade) Right externalExamShowGrades)
|
||||||
|
}
|
||||||
|
dbtCsvDecode
|
||||||
|
| mode == EEUMUsers = Just DBTCsvDecode
|
||||||
|
{ dbtCsvRowKey = \csv -> do
|
||||||
|
pid <- lift $ guessUser' csv
|
||||||
|
fmap E.Value . MaybeT . getKeyBy $ UniqueExternalExamResult eeId pid
|
||||||
|
, dbtCsvComputeActions = \case
|
||||||
|
DBCsvDiffMissing{dbCsvOldKey}
|
||||||
|
-> yield . ExternalExamUserCsvDeregisterData $ E.unValue dbCsvOldKey
|
||||||
|
DBCsvDiffNew{dbCsvNewKey = Just _}
|
||||||
|
-> error "An UniqueExternalExamResult could be found, but the ExternalExamResultKey is not among the existing keys"
|
||||||
|
DBCsvDiffNew{dbCsvNewKey = Nothing, dbCsvNew} -> do
|
||||||
|
pid <- lift $ guessUser' dbCsvNew
|
||||||
|
let ExternalExamUserTableCsv{..} = dbCsvNew
|
||||||
|
yield $ ExternalExamUserCsvRegisterData pid (zonedTimeToUTC csvEUserOccurrenceStart) csvEUserExamResult
|
||||||
|
DBCsvDiffExisting{..} -> do
|
||||||
|
let ExternalExamUserTableCsv{..} = dbCsvNew
|
||||||
|
when (zonedTimeToUTC csvEUserOccurrenceStart /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultTime) $
|
||||||
|
yield $ ExternalExamUserCsvSetTimeData (E.unValue dbCsvOldKey) (zonedTimeToUTC csvEUserOccurrenceStart)
|
||||||
|
|
||||||
|
when (csvEUserExamResult /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultResult . to resultView) $
|
||||||
|
yield $ ExternalExamUserCsvSetResultData (E.unValue dbCsvOldKey) csvEUserExamResult
|
||||||
|
, dbtCsvClassifyAction = \case
|
||||||
|
ExternalExamUserCsvRegisterData{} -> ExternalExamUserCsvRegister
|
||||||
|
ExternalExamUserCsvSetTimeData{} -> ExternalExamUserCsvSetTime
|
||||||
|
ExternalExamUserCsvSetResultData{} -> ExternalExamUserCsvSetResult
|
||||||
|
ExternalExamUserCsvDeregisterData{} -> ExternalExamUserCsvDeregister
|
||||||
|
, dbtCsvCoarsenActionClass = \case
|
||||||
|
ExternalExamUserCsvRegister -> DBCsvActionNew
|
||||||
|
ExternalExamUserCsvDeregister -> DBCsvActionMissing
|
||||||
|
_other -> DBCsvActionExisting
|
||||||
|
, dbtCsvExecuteActions = do
|
||||||
|
C.mapM_ $ \case
|
||||||
|
ExternalExamUserCsvRegisterData{..} -> do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
let res' = either (review passingGrade) id <$> externalExamUserCsvActResult
|
||||||
|
insert_ ExternalExamResult
|
||||||
|
{ externalExamResultExam = eeId
|
||||||
|
, externalExamResultUser = externalExamUserCsvActUser
|
||||||
|
, externalExamResultTime = externalExamUserCsvActTime
|
||||||
|
, externalExamResultResult = res'
|
||||||
|
, externalExamResultLastChanged = now
|
||||||
|
}
|
||||||
|
audit $ TransactionExternalExamResultEdit eeId externalExamUserCsvActUser
|
||||||
|
ExternalExamUserCsvSetTimeData{..} -> do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
ExternalExamResult{..} <- updateGet externalExamUserCsvActRegistration
|
||||||
|
[ ExternalExamResultTime =. externalExamUserCsvActTime
|
||||||
|
, ExternalExamResultLastChanged =. now
|
||||||
|
]
|
||||||
|
audit $ TransactionExternalExamResultEdit eeId externalExamResultUser
|
||||||
|
ExternalExamUserCsvSetResultData{..} -> do
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
let res' = either (review passingGrade) id <$> externalExamUserCsvActResult
|
||||||
|
ExternalExamResult{..} <- updateGet externalExamUserCsvActRegistration
|
||||||
|
[ ExternalExamResultResult =. res'
|
||||||
|
, ExternalExamResultLastChanged =. now
|
||||||
|
]
|
||||||
|
audit $ TransactionExternalExamResultEdit eeId externalExamResultUser
|
||||||
|
ExternalExamUserCsvDeregisterData{..} -> do
|
||||||
|
ExternalExamResult{..} <- getJust externalExamUserCsvActRegistration
|
||||||
|
delete externalExamUserCsvActRegistration
|
||||||
|
audit $ TransactionExternalExamResultDelete eeId externalExamResultUser
|
||||||
|
return $ EExamR tid ssh coursen examn EEUsersR
|
||||||
|
, dbtCsvRenderKey = \(registeredUserName -> registeredUserName') -> \case
|
||||||
|
ExternalExamUserCsvRegisterData{..} -> do
|
||||||
|
User{..} <- liftHandler . runDB $ getJust externalExamUserCsvActUser
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
^{nameWidget userDisplayName userSurname}
|
||||||
|
, ^{formatTimeW SelFormatDateTime externalExamUserCsvActTime}
|
||||||
|
, _{externalExamUserCsvActResult}
|
||||||
|
|]
|
||||||
|
ExternalExamUserCsvSetTimeData{..} ->
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
^{registeredUserName' externalExamUserCsvActRegistration}
|
||||||
|
, ^{formatTimeW SelFormatDateTime externalExamUserCsvActTime}
|
||||||
|
|]
|
||||||
|
ExternalExamUserCsvSetResultData{..} ->
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
^{registeredUserName' externalExamUserCsvActRegistration}
|
||||||
|
, _{externalExamUserCsvActResult}
|
||||||
|
|]
|
||||||
|
ExternalExamUserCsvDeregisterData{..} ->
|
||||||
|
registeredUserName' externalExamUserCsvActRegistration
|
||||||
|
, dbtCsvRenderActionClass = i18n
|
||||||
|
, dbtCsvRenderException = ap getMessageRender . pure :: ExamUserCsvException -> DB Text
|
||||||
|
}
|
||||||
|
| otherwise = Nothing
|
||||||
|
where
|
||||||
|
registeredUserName :: Map (E.Value ExternalExamResultId) ExternalExamUserTableData -> ExternalExamResultId -> Widget
|
||||||
|
registeredUserName existing (E.Value -> registration) = nameWidget userDisplayName userSurname
|
||||||
|
where
|
||||||
|
Entity _ User{..} = view resultUser $ existing Map.! registration
|
||||||
|
|
||||||
|
guessUser' :: ExternalExamUserTableCsv -> DB UserId
|
||||||
|
guessUser' ExternalExamUserTableCsv{..} = do
|
||||||
|
let criteria = Set.fromList $ catMaybes
|
||||||
|
[ GuessUserMatrikelnummer <$> csvEUserMatriculation
|
||||||
|
, GuessUserDisplayName <$> csvEUserName
|
||||||
|
, GuessUserSurname <$> csvEUserSurname
|
||||||
|
, GuessUserFirstName <$> csvEUserFirstName
|
||||||
|
]
|
||||||
|
maybe (throwM ExamUserCsvExceptionNoMatchingUser) return =<< guessUser criteria
|
||||||
|
externalExamUsersDBTableValidator = def
|
||||||
|
& defaultSorting (bool id (SortAscBy "is-synced" :) (mode == EEUMGrades) [SortAscBy "user-name"])
|
||||||
|
& defaultPagesize PagesizeAll
|
||||||
|
|
||||||
|
postprocess :: FormResult (First ExternalExamUserActionData, DBFormResult ExternalExamResultId Bool ExternalExamUserTableData) -> FormResult (ExternalExamUserActionData, Set ExternalExamResultId)
|
||||||
|
postprocess inp = do
|
||||||
|
(First (Just act), regMap) <- inp
|
||||||
|
let regSet = Map.keysSet . Map.filter id $ getDBFormResult (const False) regMap
|
||||||
|
return (act, regSet)
|
||||||
|
|
||||||
|
over _1 postprocess <$> dbTable externalExamUsersDBTableValidator DBTable{..}
|
||||||
|
|
||||||
|
externalExamResultMarkSynchronised :: ExternalExamResultId -> DB ()
|
||||||
|
externalExamResultMarkSynchronised resId = do
|
||||||
|
uid <- requireAuthId
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
|
||||||
|
userFunctions <- selectList [ UserFunctionUser ==. uid, UserFunctionFunction ==. SchoolExamOffice ] []
|
||||||
|
if
|
||||||
|
| null userFunctions ->
|
||||||
|
insert_ ExamOfficeExternalResultSynced
|
||||||
|
{ examOfficeExternalResultSyncedOffice = uid
|
||||||
|
, examOfficeExternalResultSyncedResult = resId
|
||||||
|
, examOfficeExternalResultSyncedTime = now
|
||||||
|
, examOfficeExternalResultSyncedSchool = Nothing
|
||||||
|
}
|
||||||
|
| otherwise ->
|
||||||
|
insertMany_ [ ExamOfficeExternalResultSynced
|
||||||
|
{ examOfficeExternalResultSyncedOffice = uid
|
||||||
|
, examOfficeExternalResultSyncedResult = resId
|
||||||
|
, examOfficeExternalResultSyncedTime = now
|
||||||
|
, examOfficeExternalResultSyncedSchool = Just userFunctionSchool
|
||||||
|
}
|
||||||
|
| Entity _ UserFunction{..} <- userFunctions
|
||||||
|
]
|
||||||
@ -134,7 +134,7 @@ data InvitationConfig junction = forall formCtx. InvitationConfig
|
|||||||
-- ^ Additional restrictions to check before allowing an user to redeem an invitation token
|
-- ^ Additional restrictions to check before allowing an user to redeem an invitation token
|
||||||
, invitationForm :: Entity (InvitationFor junction) -> InvitationData junction -> Key User -> AForm (YesodDB UniWorX) (InvitableJunction junction, formCtx)
|
, invitationForm :: Entity (InvitationFor junction) -> InvitationData junction -> Key User -> AForm (YesodDB UniWorX) (InvitableJunction junction, formCtx)
|
||||||
-- ^ Assimilate the additional data entered by the redeeming user
|
-- ^ Assimilate the additional data entered by the redeeming user
|
||||||
, invitationInsertHook :: forall a. Entity (InvitationFor junction) -> InvitationData junction -> junction -> formCtx -> (YesodJobDB UniWorX a -> YesodJobDB UniWorX a)
|
, invitationInsertHook :: forall a. UserEmail -> Entity (InvitationFor junction) -> InvitationData junction -> junction -> formCtx -> (YesodJobDB UniWorX a -> YesodJobDB UniWorX a)
|
||||||
-- ^ Perform additional actions before or after insertion of the junction into the database
|
-- ^ Perform additional actions before or after insertion of the junction into the database
|
||||||
, invitationSuccessMsg :: Entity (InvitationFor junction) -> Entity junction -> DB (SomeMessage UniWorX)
|
, invitationSuccessMsg :: Entity (InvitationFor junction) -> Entity junction -> DB (SomeMessage UniWorX)
|
||||||
-- ^ What to tell the redeeming user after accepting the invitation
|
-- ^ What to tell the redeeming user after accepting the invitation
|
||||||
@ -402,7 +402,7 @@ invitationR' InvitationConfig{..} = liftHandler $ do
|
|||||||
return . Just $ SomeRoute HomeR
|
return . Just $ SomeRoute HomeR
|
||||||
Just (jData, formCtx) -> do
|
Just (jData, formCtx) -> do
|
||||||
let junction = review _InvitableJunction (invitee, fid, jData)
|
let junction = review _InvitableJunction (invitee, fid, jData)
|
||||||
mResult <- invitationInsertHook fEnt iData junction formCtx $ insertUniqueEntity junction
|
mResult <- invitationInsertHook itEmail fEnt iData junction formCtx $ insertUniqueEntity junction
|
||||||
case mResult of
|
case mResult of
|
||||||
Nothing -> invalidArgsI [MsgInvitationCollision]
|
Nothing -> invalidArgsI [MsgInvitationCollision]
|
||||||
Just res -> do
|
Just res -> do
|
||||||
|
|||||||
@ -840,6 +840,31 @@ anchorColonnadeM mkUrl = imapColonnade anchorColonnade'
|
|||||||
anchorColonnade' inp (view dbCell -> (attrs, act)) = review dbCell . (attrs,) $
|
anchorColonnade' inp (view dbCell -> (attrs, act)) = review dbCell . (attrs,) $
|
||||||
view (dbCell . _2) . anchorCellM (mkUrl inp) =<< act
|
view (dbCell . _2) . anchorCellM (mkUrl inp) =<< act
|
||||||
|
|
||||||
|
maybeAnchorColonnade :: forall h r' m a url.
|
||||||
|
( HasRoute UniWorX url
|
||||||
|
, IsDBTable m a
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> (r' -> Maybe url)
|
||||||
|
-> Colonnade h r' (DBCell m a)
|
||||||
|
-> Colonnade h r' (DBCell m a)
|
||||||
|
maybeAnchorColonnade = maybeAnchorColonnadeM . (hoistMaybe .)
|
||||||
|
|
||||||
|
maybeAnchorColonnadeM :: forall h r' m a url.
|
||||||
|
( HasRoute UniWorX url
|
||||||
|
, IsDBTable m a
|
||||||
|
, HandlerSite m ~ UniWorX
|
||||||
|
)
|
||||||
|
=> (r' -> MaybeT (WidgetFor UniWorX) url)
|
||||||
|
-> Colonnade h r' (DBCell m a)
|
||||||
|
-> Colonnade h r' (DBCell m a)
|
||||||
|
maybeAnchorColonnadeM mkUrl = imapColonnade anchorColonnade'
|
||||||
|
where
|
||||||
|
anchorColonnade' :: r' -> DBCell m a -> DBCell m a
|
||||||
|
anchorColonnade' inp (view dbCell -> (attrs, act)) = review dbCell . (attrs,) $
|
||||||
|
view (dbCell . _2) . maybeAnchorCellM (mkUrl inp) =<< act
|
||||||
|
|
||||||
|
|
||||||
emptyOpticColonnade :: forall h r' focus c.
|
emptyOpticColonnade :: forall h r' focus c.
|
||||||
( Monoid c
|
( Monoid c
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,6 +33,7 @@ module Handler.Utils.Table.Pagination
|
|||||||
, cell, textCell, stringCell, i18nCell
|
, cell, textCell, stringCell, i18nCell
|
||||||
, anchorCell, anchorCell', anchorCellM, anchorCellM'
|
, anchorCell, anchorCell', anchorCellM, anchorCellM'
|
||||||
, linkEitherCell, linkEitherCellM, linkEitherCellM'
|
, linkEitherCell, linkEitherCellM, linkEitherCellM'
|
||||||
|
, maybeAnchorCellM, maybeAnchorCellM', maybeLinkEitherCellM'
|
||||||
, cellTooltip
|
, cellTooltip
|
||||||
, listCell
|
, listCell
|
||||||
, formCell, DBFormResult(..), getDBFormResult
|
, formCell, DBFormResult(..), getDBFormResult
|
||||||
@ -93,6 +94,8 @@ import Data.Ratio ((%))
|
|||||||
|
|
||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
|
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
|
||||||
import Data.Aeson (Options(..), SumEncoding(..), defaultOptions)
|
import Data.Aeson (Options(..), SumEncoding(..), defaultOptions)
|
||||||
import Data.Aeson.Text
|
import Data.Aeson.Text
|
||||||
import Data.Aeson.TH (deriveJSON)
|
import Data.Aeson.TH (deriveJSON)
|
||||||
@ -1300,6 +1303,12 @@ anchorCellM routeM widget = anchorCellM' routeM id (const widget)
|
|||||||
anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX x -> (x -> url) -> (x -> wgt) -> DBCell m a
|
anchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => WidgetFor UniWorX x -> (x -> url) -> (x -> wgt) -> DBCell m a
|
||||||
anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget)
|
anchorCellM' xM x2route x2widget = linkEitherCellM' xM x2route (x2widget, x2widget)
|
||||||
|
|
||||||
|
maybeAnchorCellM :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => MaybeT (WidgetFor UniWorX) url -> wgt -> DBCell m a
|
||||||
|
maybeAnchorCellM routeM widget = maybeAnchorCellM' routeM id (const widget)
|
||||||
|
|
||||||
|
maybeAnchorCellM' :: (HasRoute UniWorX url, ToWidget UniWorX wgt, IsDBTable m a) => MaybeT (WidgetFor UniWorX) x -> (x -> url) -> (Maybe x -> wgt) -> DBCell m a
|
||||||
|
maybeAnchorCellM' xM x2route x2widget = maybeLinkEitherCellM' xM x2route (x2widget . Just, x2widget)
|
||||||
|
|
||||||
-- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user
|
-- | Variant of `anchorCell` that displays different widgets depending whether the route is authorized for current user
|
||||||
linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => url -> (wgt, wgt') -> DBCell m a
|
linkEitherCell :: (HasRoute UniWorX url, ToWidget UniWorX wgt, ToWidget UniWorX wgt', IsDBTable m a) => url -> (wgt, wgt') -> DBCell m a
|
||||||
linkEitherCell = linkEitherCellM . return
|
linkEitherCell = linkEitherCellM . return
|
||||||
@ -1314,17 +1323,31 @@ linkEitherCellM' :: forall m url wgt wgt' a x.
|
|||||||
, IsDBTable m a
|
, IsDBTable m a
|
||||||
)
|
)
|
||||||
=> WidgetFor UniWorX x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
|
=> WidgetFor UniWorX x -> (x -> url) -> (x -> wgt, x -> wgt') -> DBCell m a
|
||||||
linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do
|
linkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = maybeLinkEitherCellM' (lift xM) x2route (x2widgetAuth, x2widgetUnauth . fromJust)
|
||||||
x <- xM
|
|
||||||
let route = x2route x
|
maybeLinkEitherCellM' :: forall m url wgt wgt' a x.
|
||||||
widget, widgetUnauth :: Widget
|
( HasRoute UniWorX url
|
||||||
widget = toWidget $ x2widgetAuth x
|
, ToWidget UniWorX wgt
|
||||||
widgetUnauth = toWidget $ x2widgetUnauth x
|
, ToWidget UniWorX wgt'
|
||||||
authResult <- liftHandler $ isAuthorized (urlRoute route) False
|
, IsDBTable m a
|
||||||
linkUrl <- toTextUrl route
|
)
|
||||||
case authResult of
|
=> MaybeT (WidgetFor UniWorX) x -> (x -> url) -> (x -> wgt, Maybe x -> wgt') -> DBCell m a
|
||||||
Authorized -> $(widgetFile "table/cell/link") -- show allowed link
|
maybeLinkEitherCellM' xM x2route (x2widgetAuth,x2widgetUnauth) = cell $ do
|
||||||
_otherwise -> widgetUnauth -- show alternative widget
|
x' <- runMaybeT xM
|
||||||
|
case x' of
|
||||||
|
Just x -> do
|
||||||
|
let route = x2route x
|
||||||
|
widget, widgetUnauth :: Widget
|
||||||
|
widget = toWidget $ x2widgetAuth x
|
||||||
|
widgetUnauth = toWidget . x2widgetUnauth $ Just x
|
||||||
|
authResult <- liftHandler $ isAuthorized (urlRoute route) False
|
||||||
|
linkUrl <- toTextUrl route
|
||||||
|
case authResult of
|
||||||
|
Authorized -> $(widgetFile "table/cell/link") -- show allowed link
|
||||||
|
_otherwise -> widgetUnauth -- show alternative widget
|
||||||
|
_otherwise -> do
|
||||||
|
toWidget $ x2widgetUnauth Nothing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,12 @@ module Handler.Utils.Users
|
|||||||
( computeUserAuthenticationDigest
|
( computeUserAuthenticationDigest
|
||||||
, Digest, SHA3_256
|
, Digest, SHA3_256
|
||||||
, constEq
|
, constEq
|
||||||
|
, GuessUserInfo(..)
|
||||||
|
, guessUser
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
|
import Auth.LDAP (campusUserMatr')
|
||||||
|
|
||||||
import Crypto.Hash (Digest, SHA3_256, hashlazy)
|
import Crypto.Hash (Digest, SHA3_256, hashlazy)
|
||||||
|
|
||||||
@ -12,6 +15,74 @@ import Data.ByteArray (constEq)
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
import qualified Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
|
|
||||||
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
|
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
|
||||||
computeUserAuthenticationDigest = hashlazy . JSON.encode
|
computeUserAuthenticationDigest = hashlazy . JSON.encode
|
||||||
|
|
||||||
|
|
||||||
|
data GuessUserInfo
|
||||||
|
= GuessUserMatrikelnummer { guessUserMatrikelnummer :: UserMatriculation }
|
||||||
|
| GuessUserDisplayName { guessUserDisplayName :: UserDisplayName }
|
||||||
|
| GuessUserSurname { guessUserSurname :: UserSurname }
|
||||||
|
| GuessUserFirstName { guessUserFirstName :: UserFirstName }
|
||||||
|
deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||||
|
instance Binary GuessUserInfo
|
||||||
|
|
||||||
|
makeLenses_ ''GuessUserInfo
|
||||||
|
|
||||||
|
guessUser :: Set GuessUserInfo -> DB (Maybe UserId)
|
||||||
|
guessUser (Set.toList -> criteria) = $cachedHereBinary criteria $ go False
|
||||||
|
where
|
||||||
|
toSql user = \case
|
||||||
|
GuessUserMatrikelnummer userMatriculation' -> user E.^. UserMatrikelnummer E.==. E.val (Just userMatriculation')
|
||||||
|
GuessUserDisplayName userDisplayName' -> user E.^. UserDisplayName `E.hasInfix` E.val userDisplayName'
|
||||||
|
GuessUserSurname userSurname' -> user E.^. UserSurname `E.hasInfix` E.val userSurname'
|
||||||
|
GuessUserFirstName userFirstName' -> user E.^. UserFirstName `E.hasInfix` E.val userFirstName'
|
||||||
|
|
||||||
|
go didLdap = do
|
||||||
|
let retrieveUsers = E.select . E.from $ \user -> do
|
||||||
|
E.where_ . E.and $ map (toSql user) criteria
|
||||||
|
return user
|
||||||
|
users <- retrieveUsers
|
||||||
|
let users' = sortBy (flip closeness) users
|
||||||
|
|
||||||
|
matchesMatriculation :: Entity User -> Maybe Bool
|
||||||
|
matchesMatriculation = preview $ _entityVal . _userMatrikelnummer . to (\userMatr -> all ((== userMatr) . Just) $ criteria ^.. folded . _guessUserMatrikelnummer)
|
||||||
|
|
||||||
|
closeness :: Entity User -> Entity User -> Ordering
|
||||||
|
closeness = mconcat $ concat
|
||||||
|
[ pure $ comparing (fmap Down . matchesMatriculation)
|
||||||
|
, (criteria ^.. folded . _guessUserSurname) <&> \surn -> comparing (preview $ _entityVal . _userSurname . to CI.mk . only (CI.mk surn))
|
||||||
|
, (criteria ^.. folded . _guessUserFirstName) <&> \firstn -> comparing (preview $ _entityVal . _userFirstName . to CI.mk . only (CI.mk firstn))
|
||||||
|
, (criteria ^.. folded . _guessUserDisplayName) <&> \dispn -> comparing (preview $ _entityVal . _userDisplayName . to CI.mk . only (CI.mk dispn))
|
||||||
|
]
|
||||||
|
|
||||||
|
doLdap userMatr = do
|
||||||
|
app <- getYesod
|
||||||
|
let ldap = (,) <$> app ^. _appLdapConf <*> app ^. _appLdapPool
|
||||||
|
fmap (fmap entityKey . join) . for ldap $ \(ldapConf, ldapPool) -> do
|
||||||
|
ldapData <- campusUserMatr' ldapConf ldapPool userMatr
|
||||||
|
for ldapData $ upsertCampusUser UpsertCampusUser
|
||||||
|
|
||||||
|
|
||||||
|
case users' of
|
||||||
|
x@(Entity pid _) : xs
|
||||||
|
| [] <- xs
|
||||||
|
, fromMaybe False (matchesMatriculation x) || didLdap
|
||||||
|
-> return $ Just pid
|
||||||
|
| x' : _ <- xs
|
||||||
|
, fromMaybe False (matchesMatriculation x) || didLdap
|
||||||
|
, GT <- x `closeness` x'
|
||||||
|
-> return $ Just pid
|
||||||
|
| not didLdap
|
||||||
|
, userMatr : userMatrs' <- criteria ^.. folded . _guessUserMatrikelnummer
|
||||||
|
, all (== userMatr) userMatrs'
|
||||||
|
-> doLdap userMatr >>= maybe (go True) (return . Just)
|
||||||
|
_other
|
||||||
|
-> return Nothing
|
||||||
|
|||||||
@ -307,6 +307,27 @@ determineCrontab = execWriterT $ do
|
|||||||
|
|
||||||
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ examJobs
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ examJobs
|
||||||
|
|
||||||
|
|
||||||
|
let
|
||||||
|
externalExamJobs (Entity nExternalExam ExternalExam{..}) = do
|
||||||
|
newestResult <- lift . E.select . E.from $ \externalExamResult -> do
|
||||||
|
E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. E.val nExternalExam
|
||||||
|
return . E.max_ $ externalExamResult E.^. ExternalExamResultLastChanged
|
||||||
|
|
||||||
|
case newestResult of
|
||||||
|
[E.Value (Just lastChange)] ->
|
||||||
|
tell $ HashMap.singleton
|
||||||
|
(JobCtlQueue $ JobQueueNotification NotificationExamOfficeExternalExamResults{..})
|
||||||
|
Cron
|
||||||
|
{ cronInitial = CronTimestamp . utcToLocalTimeTZ appTZ $ addUTCTime appNotificationCollateDelay lastChange
|
||||||
|
, cronRepeat = CronRepeatOnChange
|
||||||
|
, cronRateLimit = nominalDay
|
||||||
|
, cronNotAfter = Left appNotificationExpiration
|
||||||
|
}
|
||||||
|
_other -> return ()
|
||||||
|
|
||||||
|
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ externalExamJobs
|
||||||
|
|
||||||
let
|
let
|
||||||
allocationJobs (Entity nAllocation Allocation{..}) = do
|
allocationJobs (Entity nAllocation Allocation{..}) = do
|
||||||
whenIsJust allocationStaffRegisterFrom $ \staffRegisterFrom ->
|
whenIsJust allocationStaffRegisterFrom $ \staffRegisterFrom ->
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import Jobs.Queue
|
|||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
import Handler.Utils.ExamOffice.Exam
|
import Handler.Utils.ExamOffice.Exam
|
||||||
|
import Handler.Utils.ExamOffice.ExternalExam
|
||||||
|
|
||||||
|
|
||||||
dispatchJobQueueNotification :: Notification -> Handler ()
|
dispatchJobQueueNotification :: Notification -> Handler ()
|
||||||
@ -198,6 +199,12 @@ determineNotificationCandidates NotificationExamOfficeExamResultsChanged{..} =
|
|||||||
E.where_ $ examResult E.^. ExamResultId `E.in_` E.valList (Set.toList nExamResults)
|
E.where_ $ examResult E.^. ExamResultId `E.in_` E.valList (Set.toList nExamResults)
|
||||||
E.where_ $ examOfficeExamResultAuth (user E.^. UserId) examResult
|
E.where_ $ examOfficeExamResultAuth (user E.^. UserId) examResult
|
||||||
return user
|
return user
|
||||||
|
determineNotificationCandidates NotificationExamOfficeExternalExamResults{..} =
|
||||||
|
E.select . E.from $ \user -> do
|
||||||
|
E.where_ . E.exists . E.from $ \externalExamResult -> do
|
||||||
|
E.where_ $ externalExamResult E.^. ExternalExamResultExam E.==. E.val nExternalExam
|
||||||
|
E.where_ $ examOfficeExternalExamResultAuth (user E.^. UserId) externalExamResult
|
||||||
|
return user
|
||||||
determineNotificationCandidates notif@NotificationAllocationResults{..} = do
|
determineNotificationCandidates notif@NotificationAllocationResults{..} = do
|
||||||
lastExec <- fmap (fmap $ cronLastExecTime . entityVal) . getBy . UniqueCronLastExec . toJSON $ JobQueueNotification notif
|
lastExec <- fmap (fmap $ cronLastExecTime . entityVal) . getBy . UniqueCronLastExec . toJSON $ JobQueueNotification notif
|
||||||
E.select . E.from $ \user -> do
|
E.select . E.from $ \user -> do
|
||||||
@ -261,6 +268,7 @@ classifyNotification NotificationAllocationOutdatedRatings{} = return NTAll
|
|||||||
classifyNotification NotificationAllocationUnratedApplications{} = return NTAllocationUnratedApplications
|
classifyNotification NotificationAllocationUnratedApplications{} = return NTAllocationUnratedApplications
|
||||||
classifyNotification NotificationExamOfficeExamResults{} = return NTExamOfficeExamResults
|
classifyNotification NotificationExamOfficeExamResults{} = return NTExamOfficeExamResults
|
||||||
classifyNotification NotificationExamOfficeExamResultsChanged{} = return NTExamOfficeExamResultsChanged
|
classifyNotification NotificationExamOfficeExamResultsChanged{} = return NTExamOfficeExamResultsChanged
|
||||||
|
classifyNotification NotificationExamOfficeExternalExamResults{} = return NTExamOfficeExamResults
|
||||||
classifyNotification NotificationAllocationResults{} = return NTAllocationResults
|
classifyNotification NotificationAllocationResults{} = return NTAllocationResults
|
||||||
classifyNotification NotificationCourseRegistered{} = return NTCourseRegistered
|
classifyNotification NotificationCourseRegistered{} = return NTCourseRegistered
|
||||||
classifyNotification NotificationSubmissionEdited{} = return NTSubmissionEdited
|
classifyNotification NotificationSubmissionEdited{} = return NTSubmissionEdited
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
module Jobs.Handler.SendNotification.ExamOffice
|
module Jobs.Handler.SendNotification.ExamOffice
|
||||||
( dispatchNotificationExamOfficeExamResults
|
( dispatchNotificationExamOfficeExamResults
|
||||||
, dispatchNotificationExamOfficeExamResultsChanged
|
, dispatchNotificationExamOfficeExamResultsChanged
|
||||||
|
, dispatchNotificationExamOfficeExternalExamResults
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Import
|
import Import
|
||||||
@ -61,3 +62,22 @@ dispatchNotificationExamOfficeExamResultsChanged nExamResults jRecipient = do
|
|||||||
|
|
||||||
addAlternatives $
|
addAlternatives $
|
||||||
providePreferredAlternative ($(ihamletFile "templates/mail/examOffice/examResultsChanged.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
providePreferredAlternative ($(ihamletFile "templates/mail/examOffice/examResultsChanged.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
|
|
||||||
|
|
||||||
|
dispatchNotificationExamOfficeExternalExamResults :: ExternalExamId -> UserId -> Handler ()
|
||||||
|
dispatchNotificationExamOfficeExternalExamResults nExternalExam jRecipient = userMailT jRecipient $ do
|
||||||
|
ExternalExam{..} <- liftHandler . runDB $ getJust nExternalExam
|
||||||
|
replaceMailHeader "Auto-Submitted" $ Just "auto-generated"
|
||||||
|
setSubjectI $ MsgMailSubjectExamOfficeExternalExamResults externalExamCourseName externalExamExamName
|
||||||
|
|
||||||
|
MsgRenderer mr <- getMailMsgRenderer
|
||||||
|
let termDesc = mr . ShortTermIdentifier $ unTermKey externalExamTerm
|
||||||
|
tid = externalExamTerm
|
||||||
|
ssh = externalExamSchool
|
||||||
|
coursen = externalExamCourseName
|
||||||
|
examn = externalExamExamName
|
||||||
|
|
||||||
|
editNotifications <- mkEditNotifications jRecipient
|
||||||
|
|
||||||
|
addAlternatives $
|
||||||
|
providePreferredAlternative ($(ihamletFile "templates/mail/examOffice/externalExamResults.hamlet") :: HtmlUrlI18n UniWorXMessage (Route UniWorX))
|
||||||
|
|||||||
@ -6,7 +6,6 @@ module Jobs.Handler.SynchroniseLdap
|
|||||||
import Import
|
import Import
|
||||||
|
|
||||||
import qualified Data.Conduit.List as C
|
import qualified Data.Conduit.List as C
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
|
|
||||||
import Auth.LDAP
|
import Auth.LDAP
|
||||||
|
|
||||||
@ -48,11 +47,7 @@ dispatchJobSynchroniseLdapUser jUser = do
|
|||||||
$logInfoS "SynchroniseLdap" [st|Synchronising #{userIdent}|]
|
$logInfoS "SynchroniseLdap" [st|Synchronising #{userIdent}|]
|
||||||
|
|
||||||
ldapAttrs <- MaybeT $ campusUser' ldapConf ldapPool user
|
ldapAttrs <- MaybeT $ campusUser' ldapConf ldapPool user
|
||||||
void . lift $ upsertCampusUser ldapAttrs Creds
|
void . lift $ upsertCampusUser UpsertCampusUser ldapAttrs
|
||||||
{ credsIdent = CI.original userIdent
|
|
||||||
, credsPlugin = "dummy"
|
|
||||||
, credsExtra = []
|
|
||||||
}
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
throwM SynchroniseLdapNoLdap
|
throwM SynchroniseLdapNoLdap
|
||||||
where
|
where
|
||||||
|
|||||||
@ -89,6 +89,7 @@ data Notification = NotificationSubmissionRated { nSubmission :: SubmissionId }
|
|||||||
| NotificationAllocationOutdatedRatings { nAllocation :: AllocationId }
|
| NotificationAllocationOutdatedRatings { nAllocation :: AllocationId }
|
||||||
| NotificationExamOfficeExamResults { nExam :: ExamId }
|
| NotificationExamOfficeExamResults { nExam :: ExamId }
|
||||||
| NotificationExamOfficeExamResultsChanged { nExamResults :: Set ExamResultId }
|
| NotificationExamOfficeExamResultsChanged { nExamResults :: Set ExamResultId }
|
||||||
|
| NotificationExamOfficeExternalExamResults { nExternalExam :: ExternalExamId }
|
||||||
| NotificationAllocationResults { nAllocation :: AllocationId }
|
| NotificationAllocationResults { nAllocation :: AllocationId }
|
||||||
| NotificationCourseRegistered { nUser :: UserId, nCourse :: CourseId }
|
| NotificationCourseRegistered { nUser :: UserId, nCourse :: CourseId }
|
||||||
| NotificationSubmissionEdited { nInitiator :: UserId, nSubmission :: SubmissionId }
|
| NotificationSubmissionEdited { nInitiator :: UserId, nSubmission :: SubmissionId }
|
||||||
|
|||||||
@ -51,6 +51,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
|
|||||||
| AuthTutor
|
| AuthTutor
|
||||||
| AuthTutorControl
|
| AuthTutorControl
|
||||||
| AuthExamOffice
|
| AuthExamOffice
|
||||||
|
| AuthEvaluation
|
||||||
| AuthAllocationRegistered
|
| AuthAllocationRegistered
|
||||||
| AuthCourseRegistered
|
| AuthCourseRegistered
|
||||||
| AuthTutorialRegistered
|
| AuthTutorialRegistered
|
||||||
|
|||||||
15
src/Utils.hs
15
src/Utils.hs
@ -8,7 +8,7 @@ import ClassyPrelude.Yesod hiding (foldlM, Proxy, handle, catch)
|
|||||||
-- import Data.Double.Conversion.Text -- faster implementation for textPercent?
|
-- import Data.Double.Conversion.Text -- faster implementation for textPercent?
|
||||||
import qualified Data.Foldable as Fold
|
import qualified Data.Foldable as Fold
|
||||||
import Data.Foldable as Utils (foldlM, foldrM)
|
import Data.Foldable as Utils (foldlM, foldrM)
|
||||||
import Data.Monoid (Sum(..))
|
import Data.Monoid (First, Sum(..))
|
||||||
import Data.Proxy
|
import Data.Proxy
|
||||||
|
|
||||||
import Data.CaseInsensitive (CI)
|
import Data.CaseInsensitive (CI)
|
||||||
@ -381,6 +381,9 @@ setProduct :: Set a -> Set b -> Set (a, b)
|
|||||||
-- ^ Depends on the valid internal structure of the given sets
|
-- ^ Depends on the valid internal structure of the given sets
|
||||||
setProduct (Set.toAscList -> as) (Set.toAscList -> bs) = Set.fromDistinctAscList $ (,) <$> as <*> bs
|
setProduct (Set.toAscList -> as) (Set.toAscList -> bs) = Set.fromDistinctAscList $ (,) <$> as <*> bs
|
||||||
|
|
||||||
|
setPartitionEithers :: (Ord a, Ord b) => Set (Either a b) -> (Set a, Set b)
|
||||||
|
setPartitionEithers = (,) <$> setMapMaybe (preview _Left) <*> setMapMaybe (preview _Right)
|
||||||
|
|
||||||
----------
|
----------
|
||||||
-- Maps --
|
-- Maps --
|
||||||
----------
|
----------
|
||||||
@ -985,3 +988,13 @@ unstableSortOn = unstableSortBy . comparing
|
|||||||
|
|
||||||
unstableSort :: (MonadRandom m, Ord a) => [a] -> m [a]
|
unstableSort :: (MonadRandom m, Ord a) => [a] -> m [a]
|
||||||
unstableSort = unstableSortBy compare
|
unstableSort = unstableSortBy compare
|
||||||
|
|
||||||
|
----------
|
||||||
|
-- Lens --
|
||||||
|
----------
|
||||||
|
|
||||||
|
mpreview :: (MonadPlus m, MonadReader s m) => Getting (First a) s a -> m a
|
||||||
|
mpreview = hoistMaybe <=< preview
|
||||||
|
|
||||||
|
mpreviews :: (MonadPlus m, MonadReader s m) => Getting (First b) s a -> (a -> b) -> m b
|
||||||
|
mpreviews a f = hoistMaybe =<< previews a f
|
||||||
|
|||||||
@ -49,6 +49,9 @@ _nullable = prism' toNullable fromNullable
|
|||||||
_SchoolId :: Iso' SchoolId SchoolShorthand
|
_SchoolId :: Iso' SchoolId SchoolShorthand
|
||||||
_SchoolId = iso unSchoolKey SchoolKey
|
_SchoolId = iso unSchoolKey SchoolKey
|
||||||
|
|
||||||
|
_TermId :: Iso' TermId TermIdentifier
|
||||||
|
_TermId = iso unTermKey TermKey
|
||||||
|
|
||||||
_StudyTermsId :: Iso' StudyTermsId StudyTermsKey
|
_StudyTermsId :: Iso' StudyTermsId StudyTermsKey
|
||||||
_StudyTermsId = iso unStudyTermsKey StudyTermsKey'
|
_StudyTermsId = iso unStudyTermsKey StudyTermsKey'
|
||||||
|
|
||||||
@ -221,6 +224,11 @@ makeLenses_ ''Tutorial
|
|||||||
|
|
||||||
makeLenses_ ''SessionFile
|
makeLenses_ ''SessionFile
|
||||||
|
|
||||||
|
makeLenses_ ''ExternalExam
|
||||||
|
makeLenses_ ''ExternalExamOfficeSchool
|
||||||
|
makeLenses_ ''ExternalExamStaff
|
||||||
|
makeLenses_ ''ExternalExamResult
|
||||||
|
|
||||||
|
|
||||||
-- makeClassy_ ''Load
|
-- makeClassy_ ''Load
|
||||||
|
|
||||||
|
|||||||
@ -87,7 +87,6 @@ multifocusL = multifocusOptic
|
|||||||
(\s a -> [t|Lens' $(s) $(a)|])
|
(\s a -> [t|Lens' $(s) $(a)|])
|
||||||
(\doGet doSet -> [e|lens $(doGet) $(doSet)|])
|
(\doGet doSet -> [e|lens $(doGet) $(doSet)|])
|
||||||
|
|
||||||
|
|
||||||
multifocusOptic :: _ -> _ -> _ -> _ -> Natural -> ExpQ
|
multifocusOptic :: _ -> _ -> _ -> _ -> Natural -> ExpQ
|
||||||
multifocusOptic _ _ _ _ 0 = [e|united|]
|
multifocusOptic _ _ _ _ 0 = [e|united|]
|
||||||
multifocusOptic doClone _ _ _ 1 = doClone
|
multifocusOptic doClone _ _ _ 1 = doClone
|
||||||
|
|||||||
@ -2,4 +2,8 @@ $newline never
|
|||||||
<section>
|
<section>
|
||||||
^{closeWgt}
|
^{closeWgt}
|
||||||
<section>
|
<section>
|
||||||
|
$if hasUsers
|
||||||
|
<div .notification .notification-info .fa-question .notification--broad>
|
||||||
|
<div .notification__content>
|
||||||
|
_{MsgExamGradesExplanation}
|
||||||
^{examUsersTable}
|
^{examUsersTable}
|
||||||
|
|||||||
6
templates/exam-office/externalExamGrades.hamlet
Normal file
6
templates/exam-office/externalExamGrades.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
$if hasUsers
|
||||||
|
<div .notification .notification-info .fa-question .notification--broad>
|
||||||
|
<div .notification__content>
|
||||||
|
_{MsgExamGradesExplanation}
|
||||||
|
^{table}
|
||||||
62
templates/external-exam-show.hamlet
Normal file
62
templates/external-exam-show.hamlet
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
$newline never
|
||||||
|
$maybe ExternalExamResult{externalExamResultResult} <- mResult
|
||||||
|
<section>
|
||||||
|
<h2>
|
||||||
|
_{MsgExamResult}
|
||||||
|
|
||||||
|
<p .result>
|
||||||
|
$case externalExamResultResult
|
||||||
|
$of ExamAttended grade
|
||||||
|
$if externalExamShowGrades
|
||||||
|
_{grade}
|
||||||
|
$else
|
||||||
|
$if view (passingGrade . _Wrapped) grade
|
||||||
|
_{MsgExamPassed}
|
||||||
|
$else
|
||||||
|
_{MsgExamNotPassed}
|
||||||
|
$of ExamNoShow
|
||||||
|
_{MsgExamNoShow}
|
||||||
|
$of ExamVoided
|
||||||
|
_{MsgExamVoided}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>_{MsgTerm}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
_{unTermKey externalExamTerm}
|
||||||
|
<dt .deflist__dt>_{MsgCourseSchool}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
#{schoolName}
|
||||||
|
<dt .deflist__dt>_{MsgCourseName}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
#{externalExamCourseName}
|
||||||
|
<dt .deflist__dt>_{MsgExamName}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
#{externalExamExamName}
|
||||||
|
$maybe examTime <- fmap externalExamResultTime mResult <|> externalExamDefaultTime
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExamTime}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
^{formatTimeW SelFormatDateTime examTime}
|
||||||
|
$if not (null addSchools)
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExternalExamExamOfficeSchools}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
$forall Entity _ School{schoolName} <- addSchools
|
||||||
|
<li>
|
||||||
|
#{schoolName}
|
||||||
|
$if not (null staff)
|
||||||
|
<dt .deflist__dt>
|
||||||
|
_{MsgExternalExamStaff}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
$forall s <- staff
|
||||||
|
$case s
|
||||||
|
$of Right (Entity _ User{userDisplayName, userDisplayEmail, userSurname})
|
||||||
|
<li>
|
||||||
|
^{nameEmailWidget userDisplayEmail userDisplayName userSurname}
|
||||||
|
$of Left email
|
||||||
|
<li style="font-family: monospace">
|
||||||
|
#{email}
|
||||||
|
|
||||||
6
templates/external-exam/schoolMassInput/add.hamlet
Normal file
6
templates/external-exam/schoolMassInput/add.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<td>
|
||||||
|
#{csrf}
|
||||||
|
^{fvInput addView}
|
||||||
|
<td>
|
||||||
|
^{fvInput submitView}
|
||||||
3
templates/external-exam/schoolMassInput/cell.hamlet
Normal file
3
templates/external-exam/schoolMassInput/cell.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
$newline never
|
||||||
|
<td>
|
||||||
|
#{schoolName}
|
||||||
11
templates/external-exam/schoolMassInput/layout.hamlet
Normal file
11
templates/external-exam/schoolMassInput/layout.hamlet
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
$newline never
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
$forall coord <- review liveCoords lLength
|
||||||
|
<tr .massinput__cell>
|
||||||
|
^{cellWdgts ! coord}
|
||||||
|
<td>
|
||||||
|
^{fvInput (delButtons ! coord)}
|
||||||
|
<tfoot>
|
||||||
|
<tr .massinput__cell.massinput__cell--add>
|
||||||
|
^{addWdgts ! (0, 0)}
|
||||||
6
templates/external-exam/staffMassInput/add.hamlet
Normal file
6
templates/external-exam/staffMassInput/add.hamlet
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<td colspan=2>
|
||||||
|
#{csrf}
|
||||||
|
^{fvInput addView}
|
||||||
|
<td>
|
||||||
|
^{fvInput submitView}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
$newline never
|
||||||
|
<td>
|
||||||
|
<span style="font-family: monospace">
|
||||||
|
#{email}
|
||||||
|
<td>
|
||||||
|
^{messageTooltip invWarnMsg}
|
||||||
3
templates/external-exam/staffMassInput/cellKnown.hamlet
Normal file
3
templates/external-exam/staffMassInput/cellKnown.hamlet
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
$newline never
|
||||||
|
<td colspan=2>
|
||||||
|
^{nameEmailWidget userEmail userDisplayName userSurname}
|
||||||
11
templates/external-exam/staffMassInput/layout.hamlet
Normal file
11
templates/external-exam/staffMassInput/layout.hamlet
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
$newline never
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
$forall coord <- review liveCoords lLength
|
||||||
|
<tr .massinput__cell>
|
||||||
|
^{cellWdgts ! coord}
|
||||||
|
<td>
|
||||||
|
^{fvInput (delButtons ! coord)}
|
||||||
|
<tfoot>
|
||||||
|
<tr .massinput__cell.massinput__cell--add>
|
||||||
|
^{addWdgts ! (0, 0)}
|
||||||
@ -1,5 +1,16 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>
|
||||||
|
^{formatGregorianW 2020 01 17}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Eintragung von Ergebnissen für extern (nicht in Uni2work #
|
||||||
|
verwaltete) Klausuren zur Übermittlung an Prüfungsbeauftragte
|
||||||
|
<li>
|
||||||
|
Export von Listen von Kursteilnehmern zur Durchführung von #
|
||||||
|
Kursumfragen
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
^{formatGregorianW 2019 12 05}
|
^{formatGregorianW 2019 12 05}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<dl .deflist>
|
<dl .deflist>
|
||||||
|
<dt .deflist__dt>
|
||||||
|
^{formatGregorianW 2020 01 17}
|
||||||
|
<dd .deflist__dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Support for uploading results of external exams (not managed #
|
||||||
|
within Uni2work).
|
||||||
|
<li>
|
||||||
|
Export of lists of course participants
|
||||||
|
|
||||||
<dt .deflist__dt>
|
<dt .deflist__dt>
|
||||||
^{formatGregorianW 2019 12 05}
|
^{formatGregorianW 2019 12 05}
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
|
|||||||
@ -10,37 +10,52 @@ $newline never
|
|||||||
diesem Fall wird die fehlende Spalte so behandelt als enthielte sie in #
|
diesem Fall wird die fehlende Spalte so behandelt als enthielte sie in #
|
||||||
jeder Zeile eine leere Zelle).<br />
|
jeder Zeile eine leere Zelle).<br />
|
||||||
Spalten werden an ihrer Überschrift identifiziert. #
|
Spalten werden an ihrer Überschrift identifiziert. #
|
||||||
Die Überschrift darf daher nicht verändert oder entfernt werden.
|
Die Überschrift darf daher nicht verändert oder entfernt werden.<br />
|
||||||
|
Das verwendete Separator-Zeichen (Komma, Semikolon, Tabulator, #
|
||||||
|
...) wird beim Import automatisch erkannt.<br />
|
||||||
|
Beim Import wird stets die selbe Zeichenkodierung erwartet, wie #
|
||||||
|
sie auch für den CSV-Export eingestellt ist.
|
||||||
<dt .deflist__dt>Änderungen
|
<dt .deflist__dt>Änderungen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Einige Zellen können durch den Import verändert werden.<br />
|
Bei den meisten Zellen wird durch den Import der Stand der #
|
||||||
Nicht-änderbare Zellen werden ignoriert, falls diese verändert wurden.
|
Datenbank dem Inhalt der Zelle angepasst (z.B. ein #
|
||||||
|
Klausurergebnis).<br />
|
||||||
|
Bei Zellen, wo dies nicht möglich ist (z.B. die #
|
||||||
|
Maximalpunktezahl einer Teilaufgabe), werden etwaige #
|
||||||
|
Unterschiede zum Stand der Datenbank ignoriert.
|
||||||
<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.<br />
|
Es wird eine Vorschau angezeigt, bevor irgendetwas tatsächlich #
|
||||||
In der Vorschau können dann auch nur teilweise Änderungen ausgewählt werden.
|
geändert wird.<br />
|
||||||
|
In der Vorschau kann dann auch eine beliebige Teilmenge der #
|
||||||
|
Änderungen zur Anwendung ausgewählt werden.
|
||||||
<dt .deflist__dt>Leere Zellen
|
<dt .deflist__dt>Leere Zellen
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Löschbare Zellen werden durch leere Zellen gelöscht oder auf eindeutige Werte gesetzt.
|
Löschbare Zellen werden durch leere Zellen gelöscht oder auf #
|
||||||
|
eindeutige Werte gesetzt.
|
||||||
<dt .deflist__dt>Konsistenz
|
<dt .deflist__dt>Konsistenz
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
<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 #
|
||||||
ändert man z.B. die Studienfachzuordnung eines Teilnehmers ab, #
|
zu lassen; ändert man z.B. die Studienfachzuordnung eines #
|
||||||
so müsste man auch Abschluss und Fachsemester passend ändern.<br />
|
Teilnehmers ab, so müsste man auch Abschluss und #
|
||||||
Da diese jedoch eindeutig sind, kann man diese Zellen einfach frei lassen.
|
Fachsemester passend ändern.<br />
|
||||||
|
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.<br />
|
Mehrere Spalten werden zur Identifikation der Zeile #
|
||||||
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden sein, #
|
verwendet.<br />
|
||||||
so lange die Identifikation noch eindeutig ist.<br />
|
Es muss nicht in jeder Spalte der Zeile ein Wert vorhanden #
|
||||||
Sind mehrere Werte vorhanden, so müssen diese natürlich zueinander passen.
|
sein, so lange die Identifikation noch eindeutig ist.<br />
|
||||||
|
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, sofern ausreichend #
|
Es können auch neue Zeilen hinzugefügt werden, sofern #
|
||||||
eindeutige Informationen vorhanden sind; #
|
ausreichend 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>
|
||||||
|
|||||||
@ -10,11 +10,20 @@ $newline never
|
|||||||
the column will be treated as if every cell contained within it was #
|
the column will be treated as if every cell contained within it was #
|
||||||
empty).<br />
|
empty).<br />
|
||||||
Columns are identified based on their heading. #
|
Columns are identified based on their heading. #
|
||||||
Thus column headings may not be modified or removed.
|
Thus column headings may not be modified or removed.<br />
|
||||||
|
The separator character (comma, semicolon, tabulator, ...) is #
|
||||||
|
detected automatically during import.<br />
|
||||||
|
Imported files are expected to use the character encoding, that #
|
||||||
|
is configured for CSV-export.
|
||||||
<dt .deflist__dt>Edits
|
<dt .deflist__dt>Edits
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Some cells can be changed when importing.<br />
|
For most cells, importing changes the current state of the #
|
||||||
Cells that cannot be changed are ignored, if they were changed.
|
database to reflect the content of the imported cell (i.e. exam #
|
||||||
|
results).<br />
|
||||||
|
For some cells this is not possible (i.e. the maximum number of #
|
||||||
|
points of an exam part). #
|
||||||
|
In that case all differences between the current state of the #
|
||||||
|
database and the content of the imported cell are ignored.
|
||||||
<dt .deflist__dt>Preview
|
<dt .deflist__dt>Preview
|
||||||
<dd .deflist__dd>
|
<dd .deflist__dd>
|
||||||
Before any edits are applied a preview is shown of what would be done.<br />
|
Before any edits are applied a preview is shown of what would be done.<br />
|
||||||
|
|||||||
18
templates/mail/examOffice/externalExamResults.hamlet
Normal file
18
templates/mail/examOffice/externalExamResults.hamlet
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
$newline never
|
||||||
|
\<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-variant: small-caps;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
<body>
|
||||||
|
<h1>
|
||||||
|
_{MsgMailExamOfficeExternalExamResultsIntro coursen termDesc externalExamExamName}
|
||||||
|
<p>
|
||||||
|
<a href=@{EExamR tid ssh coursen examn EEGradesR}>
|
||||||
|
#{externalExamExamName}
|
||||||
|
^{editNotifications}
|
||||||
17
templates/participants-list.hamlet
Normal file
17
templates/participants-list.hamlet
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
$newline never
|
||||||
|
<div .scrolltable .scrolltable--bordered>
|
||||||
|
<table .table .table--striped .table--hover>
|
||||||
|
<thead>
|
||||||
|
<tr .table__row .table__row--head>
|
||||||
|
<th .table__th>_{MsgTerm}
|
||||||
|
$forall school <- schools
|
||||||
|
<th .table__th>#{unSchoolKey school}
|
||||||
|
<tbody>
|
||||||
|
$forall term <- terms
|
||||||
|
<tr .table__row>
|
||||||
|
<th .table__th>_{ShortTermIdentifier (unTermKey term)}
|
||||||
|
$forall school <- schools
|
||||||
|
<td .table__td>
|
||||||
|
$if Set.member (school, term) schoolTerms
|
||||||
|
<a href=@{ParticipantsR term school}>
|
||||||
|
#{iconFileCSV}
|
||||||
@ -1,7 +1,10 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<div .breadcrumbs__container>
|
<div .breadcrumbs__container>
|
||||||
<ul .breadcrumbs__list.list--inline>
|
<ul .breadcrumbs__list.list--inline>
|
||||||
$forall bc <- parents
|
$forall (bcRoute, bcTitle, hasAccess) <- parents
|
||||||
<li .breadcrumbs__item>
|
<li .breadcrumbs__item>
|
||||||
<a .breadcrumbs__link href="@{fst bc}">#{snd bc}
|
$if hasAccess
|
||||||
|
<a .breadcrumbs__link href="@{bcRoute}">#{bcTitle}
|
||||||
|
$else
|
||||||
|
<span .breadcrumbs__link>#{bcTitle}
|
||||||
<li .breadcrumbs__last-item>#{title}
|
<li .breadcrumbs__last-item>#{title}
|
||||||
|
|||||||
63
templates/widgets/breadcrumbs/breadcrumbs.lucius
Normal file
63
templates/widgets/breadcrumbs/breadcrumbs.lucius
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
.breadcrumbs__container {
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
padding: 4px 13px;
|
||||||
|
background-color: var(--color-dark);
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 426px) {
|
||||||
|
.breadcrumbs__container {
|
||||||
|
padding: 7px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.breadcrumbs__container {
|
||||||
|
padding: 7px 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.breadcrumbs__link {
|
||||||
|
color: var(--color-lightwhite);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item {
|
||||||
|
padding-right: 14px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 28px;
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 1;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
right: 0;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-color: var(--color-white);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__last-item {
|
||||||
|
line-height: 28px;
|
||||||
|
vertical-align: bottom;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -70,6 +70,10 @@ instance Arbitrary ExamOfficeR where
|
|||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|
||||||
|
instance Arbitrary EExamR where
|
||||||
|
arbitrary = genericArbitrary
|
||||||
|
shrink = genericShrink
|
||||||
|
|
||||||
instance Arbitrary CourseNewsR where
|
instance Arbitrary CourseNewsR where
|
||||||
arbitrary = genericArbitrary
|
arbitrary = genericArbitrary
|
||||||
shrink = genericShrink
|
shrink = genericShrink
|
||||||
|
|||||||
@ -2,9 +2,10 @@ const webpack = require('webpack');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const tmp = require('tmp');
|
const tmp = require('tmp');
|
||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
const fs = require('fs');
|
const fs = require('fs-extra');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
const request = require('request-promise');
|
||||||
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
@ -17,244 +18,316 @@ const HashOutput = require('webpack-plugin-hash-output');
|
|||||||
const postcssPresetEnv = require('postcss-preset-env');
|
const postcssPresetEnv = require('postcss-preset-env');
|
||||||
const RemovePlugin = require('remove-files-webpack-plugin');
|
const RemovePlugin = require('remove-files-webpack-plugin');
|
||||||
const RealFaviconPlugin = require('real-favicon-webpack-plugin');
|
const RealFaviconPlugin = require('real-favicon-webpack-plugin');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.');
|
const webpackVersion = require('webpack/package.json').version.split('.').slice(0, 2).join('.');
|
||||||
const packageVersion = require('./package.json').version;
|
const packageVersion = require('./package.json').version;
|
||||||
|
|
||||||
module.exports = {
|
async function webpackConfig() {
|
||||||
module: {
|
let faviconApiVersion = undefined;
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
|
|
||||||
options: {
|
try {
|
||||||
plugins: ['syntax-dynamic-import'],
|
const faviconApiChangelog = await request({
|
||||||
|
method: 'GET',
|
||||||
|
uri: 'https://realfavicongenerator.net/api/versions',
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*'
|
||||||
|
},
|
||||||
|
json: true
|
||||||
|
});
|
||||||
|
faviconApiVersion = faviconApiChangelog.filter(vObj => vObj.relevance.automated_update).slice(-1)[0].version;
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
|
||||||
presets: [
|
options: {
|
||||||
[
|
plugins: ['syntax-dynamic-import'],
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
presets: [
|
||||||
modules: false,
|
[
|
||||||
targets: {
|
'@babel/preset-env',
|
||||||
edge: "17",
|
{
|
||||||
firefox: "50",
|
modules: false,
|
||||||
chrome: "60",
|
targets: {
|
||||||
safari: "11.1",
|
edge: "17",
|
||||||
ie: "11",
|
firefox: "50",
|
||||||
},
|
chrome: "60",
|
||||||
useBuiltIns: "usage",
|
safari: "11.1",
|
||||||
corejs: 3
|
ie: "11",
|
||||||
}
|
},
|
||||||
|
useBuiltIns: "usage",
|
||||||
|
corejs: 3
|
||||||
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
|
||||||
},
|
|
||||||
test: /\.js$/i,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: [ MiniCssExtractPlugin.loader,
|
|
||||||
{ loader: 'css-loader', options: { sourceMap: true }},
|
|
||||||
{ loader: 'postcss-loader', options: {
|
|
||||||
sourceMap: true,
|
|
||||||
plugins: () => [ postcssPresetEnv ]
|
|
||||||
}},
|
|
||||||
{ loader: 'resolve-url-loader', options: { sourceMap: true }}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.s(c|a)ss$/i,
|
|
||||||
use: [ MiniCssExtractPlugin.loader,
|
|
||||||
{ loader: 'css-loader', options: { sourceMap: true }},
|
|
||||||
{ loader: 'postcss-loader', options: {
|
|
||||||
sourceMap: true,
|
|
||||||
plugins: () => [ postcssPresetEnv ]
|
|
||||||
}},
|
|
||||||
{ loader: 'resolve-url-loader', options: { sourceMap: true }},
|
|
||||||
{ loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: true }}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff(2)?|ttf|eot|svg)(\?.*)?$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: '[contenthash].[ext]',
|
|
||||||
esModule: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
entry: {
|
|
||||||
main: [ path.resolve(__dirname, 'frontend/src', 'polyfill.js'),
|
|
||||||
path.resolve(__dirname, 'frontend/src', 'main.js')
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new HashOutput({
|
|
||||||
validateOutput: true,
|
|
||||||
validateOutputRegex: /static\/wp-[^\/]\//
|
|
||||||
}),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
// Options similar to the same options in webpackOptions.output
|
|
||||||
// all options are optional
|
|
||||||
filename: '[chunkhash].css',
|
|
||||||
chunkFilename: '[chunkhash].css',
|
|
||||||
ignoreOrder: false, // Enable to remove warnings about conflicting order
|
|
||||||
}),
|
|
||||||
new webpack.NamedChunksPlugin((chunk) => {
|
|
||||||
if (chunk.name) {
|
|
||||||
return chunk.name;
|
|
||||||
}
|
|
||||||
let modules = chunk.modules || [chunk.entryModule];
|
|
||||||
return modules.map(m => path.relative(m.context, m.request)).join("_");
|
|
||||||
}),
|
|
||||||
new webpack.NamedModulesPlugin(),
|
|
||||||
new ManifestPlugin({
|
|
||||||
fileName: path.resolve(__dirname, 'config', 'webpack.yml'),
|
|
||||||
publicPath: `wp-${webpackVersion}/`,
|
|
||||||
generate: (seed, files, entrypoints) => Object.keys(entrypoints).reduce((acc, fs) => ({...acc, [fs]: files.filter(file => entrypoints[fs].filter(basename => !(/\.map$/.test(basename))).some(basename => file.path.endsWith(basename))).filter(file => file.isInitial).map(file => file.path)}), {}),
|
|
||||||
serialize: yaml.safeDump
|
|
||||||
}),
|
|
||||||
new CleanWebpackPlugin({
|
|
||||||
cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, 'static'),
|
|
||||||
path.resolve(__dirname, 'well-known'),
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
|
||||||
new CopyPlugin([
|
|
||||||
{ from: 'assets/lmu/sigillum.svg', to: path.resolve(__dirname, 'static', 'img/lmu/sigillum.svg') },
|
|
||||||
]),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
VERSION: JSON.stringify(packageVersion)
|
|
||||||
}),
|
|
||||||
...(() => {
|
|
||||||
const faviconJson = require('./config/favicon.json');
|
|
||||||
const langs = new Set();
|
|
||||||
function findLangs(json) {
|
|
||||||
if (json && json._i18n) {
|
|
||||||
Object.keys(json).forEach(key => {
|
|
||||||
if (key !== '_i18n') {
|
|
||||||
langs.add(key);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (Array.isArray(json)) {
|
|
||||||
json.forEach(elem => findLangs(elem));
|
|
||||||
} else if (typeof json === 'object') {
|
|
||||||
Object.keys(json).forEach(key => findLangs(json[key]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findLangs(faviconJson);
|
|
||||||
|
|
||||||
function selectLang(lang, json) {
|
|
||||||
if (json && json._i18n) {
|
|
||||||
return json[lang];
|
|
||||||
} else if (Array.isArray(json)) {
|
|
||||||
return json.map(elem => selectLang(lang, elem));
|
|
||||||
} else if (typeof json === 'object') {
|
|
||||||
return Object.fromEntries(Object.entries(json).map(([k, v]) => [k, selectLang(lang, v)]));
|
|
||||||
} else {
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const langJsons = {};
|
|
||||||
Array.from(langs).forEach(lang => {
|
|
||||||
langJsons[lang] = selectLang(lang, faviconJson);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(langs).map(lang => {
|
|
||||||
const tmpobj = tmp.fileSync({ dir: ".", postfix: ".json" });
|
|
||||||
fs.writeSync(tmpobj.fd, JSON.stringify(langJsons[lang]));
|
|
||||||
fs.close(tmpobj.fd);
|
|
||||||
|
|
||||||
return [
|
|
||||||
new RealFaviconPlugin({
|
|
||||||
faviconJson: `./${tmpobj.name}`,
|
|
||||||
outputPath: path.resolve(__dirname, 'well-known', lang),
|
|
||||||
inject: false
|
|
||||||
}),
|
|
||||||
new CopyPlugin([
|
|
||||||
{ from: 'config/robots.txt', to: path.resolve(__dirname, 'well-known', lang, 'robots.txt') },
|
|
||||||
])
|
|
||||||
];
|
|
||||||
}).flat(1);
|
|
||||||
})(),
|
|
||||||
{ apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => {
|
|
||||||
const imgFiles = glob.sync(path.resolve(__dirname, 'well-known', '**', '*.@(png)'));
|
|
||||||
const imgFilesArgs = Array.from(imgFiles).join(" ");
|
|
||||||
execSync(`exiftool -overwrite_original -all= ${imgFilesArgs}`, { stdio: 'inherit' });
|
|
||||||
})
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
output: {
|
|
||||||
chunkFilename: '[chunkhash].js',
|
|
||||||
filename: '[chunkhash].js',
|
|
||||||
path: path.resolve(__dirname, 'static', `wp-${webpackVersion}`),
|
|
||||||
publicPath: `/static/res/wp-${webpackVersion}/`,
|
|
||||||
hashFunction: 'shake256',
|
|
||||||
hashDigestLength: 36
|
|
||||||
},
|
|
||||||
|
|
||||||
optimization: {
|
|
||||||
minimize: true,
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
cache: true,
|
|
||||||
parallel: true,
|
|
||||||
sourceMap: true
|
|
||||||
}),
|
|
||||||
new OptimizeCSSAssetsPlugin({
|
|
||||||
cssProcessorOptions: {
|
|
||||||
map: {
|
|
||||||
inline: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
runtimeChunk: 'single',
|
|
||||||
splitChunks: {
|
|
||||||
chunks: 'all',
|
|
||||||
maxInitialRequests: Infinity,
|
|
||||||
maxAsyncRequests: Infinity,
|
|
||||||
minSize: 0,
|
|
||||||
minChunks: 1,
|
|
||||||
cacheGroups: {
|
|
||||||
vendor: {
|
|
||||||
test(module, chunk) {
|
|
||||||
return module.context.match(/[\\/]node_modules[\\/]/);
|
|
||||||
},
|
},
|
||||||
name(module, chunks, cacheGroupKey) {
|
test: /\.js$/i,
|
||||||
const moduleFileName = module.identifier().split('/').reduceRight(item => item);
|
exclude: /node_modules/,
|
||||||
const allChunksNames = chunks.map((item) => item.name).join('~');
|
|
||||||
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
|
|
||||||
|
|
||||||
return `${cacheGroupKey}-${packageName}-${allChunksNames}-${moduleFileName}`;
|
|
||||||
},
|
|
||||||
priority: -10
|
|
||||||
},
|
},
|
||||||
default: {
|
{
|
||||||
priority: -20,
|
test: /\.css$/i,
|
||||||
minChunks: 1
|
use: [ MiniCssExtractPlugin.loader,
|
||||||
|
{ loader: 'css-loader', options: { sourceMap: true }},
|
||||||
|
{ loader: 'postcss-loader', options: {
|
||||||
|
sourceMap: true,
|
||||||
|
plugins: () => [ postcssPresetEnv ]
|
||||||
|
}},
|
||||||
|
{ loader: 'resolve-url-loader', options: { sourceMap: true }}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.s(c|a)ss$/i,
|
||||||
|
use: [ MiniCssExtractPlugin.loader,
|
||||||
|
{ loader: 'css-loader', options: { sourceMap: true }},
|
||||||
|
{ loader: 'postcss-loader', options: {
|
||||||
|
sourceMap: true,
|
||||||
|
plugins: () => [ postcssPresetEnv ]
|
||||||
|
}},
|
||||||
|
{ loader: 'resolve-url-loader', options: { sourceMap: true }},
|
||||||
|
{ loader: 'sass-loader', options: { implementation: require('sass'), sourceMap: true }}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff(2)?|ttf|eot|svg)(\?.*)?$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[contenthash].[ext]',
|
||||||
|
esModule: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
moduleIds: 'hashed'
|
|
||||||
},
|
|
||||||
|
|
||||||
mode: 'production',
|
entry: {
|
||||||
|
main: [ path.resolve(__dirname, 'frontend/src', 'polyfill.js'),
|
||||||
|
path.resolve(__dirname, 'frontend/src', 'main.js')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
recordsPath: path.join(__dirname, 'records.json'),
|
plugins: [
|
||||||
|
new HashOutput({
|
||||||
|
validateOutput: true,
|
||||||
|
validateOutputRegex: /static\/wp-[^\/]\//
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
// Options similar to the same options in webpackOptions.output
|
||||||
|
// all options are optional
|
||||||
|
filename: '[chunkhash].css',
|
||||||
|
chunkFilename: '[chunkhash].css',
|
||||||
|
ignoreOrder: false, // Enable to remove warnings about conflicting order
|
||||||
|
}),
|
||||||
|
new webpack.NamedChunksPlugin((chunk) => {
|
||||||
|
if (chunk.name) {
|
||||||
|
return chunk.name;
|
||||||
|
}
|
||||||
|
let modules = chunk.modules || [chunk.entryModule];
|
||||||
|
return modules.map(m => path.relative(m.context, m.request)).join("_");
|
||||||
|
}),
|
||||||
|
new webpack.NamedModulesPlugin(),
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: path.resolve(__dirname, 'config', 'webpack.yml'),
|
||||||
|
publicPath: `wp-${webpackVersion}/`,
|
||||||
|
generate: (seed, files, entrypoints) => Object.keys(entrypoints).reduce((acc, fs) => ({...acc, [fs]: files.filter(file => entrypoints[fs].filter(basename => !(/\.map$/.test(basename))).some(basename => file.path.endsWith(basename))).filter(file => file.isInitial).map(file => file.path)}), {}),
|
||||||
|
serialize: yaml.safeDump
|
||||||
|
}),
|
||||||
|
new CleanWebpackPlugin({
|
||||||
|
cleanOnceBeforeBuildPatterns: [ path.resolve(__dirname, 'static'),
|
||||||
|
path.resolve(__dirname, 'well-known'),
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||||
|
new CopyPlugin([
|
||||||
|
{ from: 'assets/lmu/sigillum.svg', to: path.resolve(__dirname, 'static', 'img/lmu/sigillum.svg') },
|
||||||
|
]),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
VERSION: JSON.stringify(packageVersion)
|
||||||
|
}),
|
||||||
|
...(() => {
|
||||||
|
const faviconJson = require('./config/favicon.json');
|
||||||
|
const langs = new Set();
|
||||||
|
function findLangs(json) {
|
||||||
|
if (json && json._i18n) {
|
||||||
|
Object.keys(json).forEach(key => {
|
||||||
|
if (key !== '_i18n') {
|
||||||
|
langs.add(key);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (Array.isArray(json)) {
|
||||||
|
json.forEach(elem => findLangs(elem));
|
||||||
|
} else if (typeof json === 'object') {
|
||||||
|
Object.keys(json).forEach(key => findLangs(json[key]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
findLangs(faviconJson);
|
||||||
|
|
||||||
performance: {
|
function selectLang(lang, json) {
|
||||||
assetFilter: (assetFilename) => !(/\.(map|svg|ttf|eot)$/.test(assetFilename))
|
if (json && json._i18n) {
|
||||||
},
|
return json[lang];
|
||||||
|
} else if (Array.isArray(json)) {
|
||||||
|
return json.map(elem => selectLang(lang, elem));
|
||||||
|
} else if (typeof json === 'object') {
|
||||||
|
return Object.fromEntries(Object.entries(json).map(([k, v]) => [k, selectLang(lang, v)]));
|
||||||
|
} else {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
devtool: 'source-map'
|
const langJsons = {};
|
||||||
};
|
Array.from(langs).forEach(lang => {
|
||||||
|
langJsons[lang] = selectLang(lang, faviconJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheHash = crypto.createHash('sha256');
|
||||||
|
cacheHash.update(JSON.stringify(langJsons));
|
||||||
|
|
||||||
|
const cacheFiles = new Set([
|
||||||
|
...(Array.from(langs).map(lang => path.resolve(__dirname, langJsons[lang].masterPicture))),
|
||||||
|
path.resolve(__dirname, 'config/robots.txt')
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const cacheFile of cacheFiles) {
|
||||||
|
cacheHash.update(fs.readFileSync(cacheFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheDigest = cacheHash.copy().digest('hex');
|
||||||
|
|
||||||
|
let cachedVersion = undefined;
|
||||||
|
|
||||||
|
const versionFile = path.resolve(__dirname, '.well-known-cache', `${cacheDigest}.version`);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(versionFile)) {
|
||||||
|
cachedVersion = fs.readFileSync(versionFile, 'utf8');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faviconApiVersion) {
|
||||||
|
cacheHash.update(faviconApiVersion);
|
||||||
|
}
|
||||||
|
const versionDigest = cacheHash.digest('hex');
|
||||||
|
|
||||||
|
return Array.from(langs).map(lang => {
|
||||||
|
const faviconConfig = { versioning: { param_name: 'v', param_value: versionDigest.substr(0,10) }, ...langJsons[lang] };
|
||||||
|
|
||||||
|
const cacheDirectory = path.resolve(__dirname, '.well-known-cache', `${cacheDigest}-${lang}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(cacheDirectory) && (!faviconApiVersion || faviconApiVersion === cachedVersion)) {
|
||||||
|
console.log(`Using cached well-known from ${cacheDirectory} for ${lang}`);
|
||||||
|
return [
|
||||||
|
new CopyPlugin([
|
||||||
|
{ from: cacheDirectory, to: path.resolve(__dirname, 'well-known', lang) }
|
||||||
|
])
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const tmpobj = tmp.fileSync({ dir: ".", postfix: ".json" });
|
||||||
|
fs.writeSync(tmpobj.fd, JSON.stringify(faviconConfig));
|
||||||
|
fs.close(tmpobj.fd);
|
||||||
|
|
||||||
|
return [
|
||||||
|
new RealFaviconPlugin({
|
||||||
|
faviconJson: `./${tmpobj.name}`,
|
||||||
|
outputPath: path.resolve(__dirname, 'well-known', lang),
|
||||||
|
inject: false
|
||||||
|
}),
|
||||||
|
new CopyPlugin([
|
||||||
|
{ from: 'config/robots.txt', to: path.resolve(__dirname, 'well-known', lang, 'robots.txt') },
|
||||||
|
]),
|
||||||
|
{ apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => {
|
||||||
|
const imgFiles = glob.sync(path.resolve(__dirname, 'well-known', lang, '*.@(png)'));
|
||||||
|
const imgFilesArgs = Array.from(imgFiles).join(" ");
|
||||||
|
execSync(`exiftool -overwrite_original -all= ${imgFilesArgs}`, { stdio: 'inherit' });
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ apply: compiler => compiler.hooks.afterEmit.tap('AfterEmitPlugin', compilation => {
|
||||||
|
fs.ensureDirSync(__dirname, '.well-known-cache');
|
||||||
|
fs.copySync(path.resolve(__dirname, 'well-known', lang), cacheDirectory);
|
||||||
|
if (!fs.existsSync(versionFile)) {
|
||||||
|
fs.writeFileSync(versionFile, faviconApiVersion, { encoding: 'utf8' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}).flat(1);
|
||||||
|
})()
|
||||||
|
],
|
||||||
|
|
||||||
|
output: {
|
||||||
|
chunkFilename: '[chunkhash].js',
|
||||||
|
filename: '[chunkhash].js',
|
||||||
|
path: path.resolve(__dirname, 'static', `wp-${webpackVersion}`),
|
||||||
|
publicPath: `/static/res/wp-${webpackVersion}/`,
|
||||||
|
hashFunction: 'shake256',
|
||||||
|
hashDigestLength: 36
|
||||||
|
},
|
||||||
|
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
cache: true,
|
||||||
|
parallel: true,
|
||||||
|
sourceMap: true
|
||||||
|
}),
|
||||||
|
new OptimizeCSSAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
map: {
|
||||||
|
inline: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
runtimeChunk: 'single',
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
maxInitialRequests: Infinity,
|
||||||
|
maxAsyncRequests: Infinity,
|
||||||
|
minSize: 0,
|
||||||
|
minChunks: 1,
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
test(module, chunk) {
|
||||||
|
return module.context.match(/[\\/]node_modules[\\/]/);
|
||||||
|
},
|
||||||
|
name(module, chunks, cacheGroupKey) {
|
||||||
|
const moduleFileName = module.identifier().split('/').reduceRight(item => item);
|
||||||
|
const allChunksNames = chunks.map((item) => item.name).join('~');
|
||||||
|
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
|
||||||
|
|
||||||
|
return `${cacheGroupKey}-${packageName}-${allChunksNames}-${moduleFileName}`;
|
||||||
|
},
|
||||||
|
priority: -10
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
priority: -20,
|
||||||
|
minChunks: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moduleIds: 'hashed'
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: 'production',
|
||||||
|
|
||||||
|
recordsPath: path.join(__dirname, 'records.json'),
|
||||||
|
|
||||||
|
performance: {
|
||||||
|
assetFilter: (assetFilename) => !(/\.(map|svg|ttf|eot)$/.test(assetFilename))
|
||||||
|
},
|
||||||
|
|
||||||
|
devtool: 'source-map'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = webpackConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user