Merge branch 'master' into workflows

This commit is contained in:
Gregor Kleen 2020-11-05 12:19:18 +01:00
commit 03fec7752b
66 changed files with 1450 additions and 244 deletions

View File

@ -2,6 +2,59 @@
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.
### [21.0.3](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.0.2...v21.0.3) (2020-11-05)
### Bug Fixes
* **mails:** prevent emails being resent to due archiving errors ([8cf39dc](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/8cf39dcbe68cefcc50691ae8a7194315d18420d6))
### [21.0.2](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.0.1...v21.0.2) (2020-11-04)
### Bug Fixes
* build ([fa61b46](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/fa61b46d308753354623df17241b5312f324321e))
### [21.0.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v21.0.0...v21.0.1) (2020-11-04)
### Bug Fixes
* **mail:** better separation of sender/from/envelope-from ([0dbf4f8](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/0dbf4f8bde99431cafeec954dc164a73227154ad))
## [21.0.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.14.0...v21.0.0) (2020-11-04)
### ⚠ BREAKING CHANGES
* **course:** AccessPredicates now take continuation
### Features
* **course:** warning if re-registration is not possible ([4451cee](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/4451ceedf7bde0da7f3bb4c0818b79d7c5df1cbd)), closes [#646](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/646)
* **mail:** archive all sent mail & better verp ([1666081](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1666081fea0eec0bf5440a100db0e8cc69be8295))
### Bug Fixes
* **course:** don't delete applications when deregistering ([b666408](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/b6664089f75dcb3b2c89dbd2941c064e8aa86404)), closes [#648](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/648)
* **courses:** better defaults for application/registration ([1c2c8fe](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c2c8fe3d99176e079d0473dd45039b44128c491))
## [20.14.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.13.0...v20.14.0) (2020-11-02)
### Features
* **users:** assimilation ([ef51c6e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ef51c6e7c34effa691125e4313876d95feda96af))
### Bug Fixes
* **exam-users:** prevent exam results without registration via csv ([1c6ac4c](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/1c6ac4cb4a52ac7e69e615e0e3ff96432b173962))
* work around conduit-bug releasing fh to early ([3ff2cf1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/3ff2cf1fec1bf582fe1d5e1f6ee08dcc85d6bc00))
* **exams:** error messages for foreign key constraint violations ([ca29a66](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/ca29a66330a977a1f28bbdbe9a733aef10371427))
## [20.13.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.1...v20.13.0) (2020-10-20) ## [20.13.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v20.12.1...v20.13.0) (2020-10-20)

View File

@ -18,10 +18,11 @@ mail-from:
mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost" mail-object-domain: "_env:MAILOBJECT_DOMAIN:localhost"
mail-verp: mail-verp:
separator: "_env:VERP_SEPARATOR:+" separator: "_env:VERP_SEPARATOR:+"
at-replacement: "_env:VERP_AT_REPLACEMENT:=" prefix: "_env:VERP_PREFIX:bounce"
mail-support: mail-support:
name: "_env:MAILSUPPORT_NAME:" name: "_env:MAILSUPPORT_NAME:"
email: "_env:MAILSUPPORT:uni2work@ifi.lmu.de" email: "_env:MAILSUPPORT:uni2work@ifi.lmu.de"
mail-retain-sent: 31470547
job-workers: "_env:JOB_WORKERS:10" job-workers: "_env:JOB_WORKERS:10"
job-flush-interval: "_env:JOB_FLUSH:30" job-flush-interval: "_env:JOB_FLUSH:30"

View File

@ -723,6 +723,10 @@ section
.uuid, .pseudonym, .ldap-primary-key, .email, .file-path, .metric-value, .metric-label, .workflow-payload--text .uuid, .pseudonym, .ldap-primary-key, .email, .file-path, .metric-value, .metric-label, .workflow-payload--text
font-family: var(--font-monospace) font-family: var(--font-monospace)
.shown
font-family: var(--font-monospace)
white-space: pre-wrap
.token .token
font-family: var(--font-monospace) font-family: var(--font-monospace)
white-space: pre-wrap white-space: pre-wrap

View File

@ -212,7 +212,9 @@ CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese si
CourseApplicationTemplate: Bewerbungsvorlagen CourseApplicationTemplate: Bewerbungsvorlagen
CourseApplicationTemplateTip: Werden den Studierenden zum download angeboten, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden CourseApplicationTemplateTip: Werden den Studierenden zum download angeboten, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseApplicationsText: Text-Bewerbungen CourseApplicationsText: Text-Bewerbungen
CourseApplicationsTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen? CourseApplicationsTextTip: Sollen die Studierenden bei Ihrer Bewerbung bzw. Anmeldung (ggf. zusätzlich zu abgegebenen Dateien) auch unformatierten Text einreichen können?
CourseApplicationsFiles: Bewerbungsdateien
CourseApplicationsFilesTip: Sollen die Studierenden bei Ihrer Bewerbung bzw. Anmeldung (ggf. zusätzlich zu unformatiertem Text) auch Dateien abgeben können?
CourseApplicationRatingsVisible: Feedback für Bewerbungen CourseApplicationRatingsVisible: Feedback für Bewerbungen
CourseApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden? CourseApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden?
CourseApplicationRequired: Bewerbungsverfahren CourseApplicationRequired: Bewerbungsverfahren
@ -225,6 +227,8 @@ CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand
CourseApplication: Bewerbung CourseApplication: Bewerbung
CourseApplicationIsParticipant: Kursteilnehmer CourseApplicationIsParticipant: Kursteilnehmer
CourseApplicationInstructionsRecommended: Studierende können bei ihrer Anmeldung/Bewerbung nach aktuellen Einstellungen Texte bzw. Dateien abgeben. Es wurden jedoch keine Anweisungen zur Bewerbung oder Vorlage-Dateien hinterlegt. Sie sollten entweder keine Texte bzw. Dateien verlangen oder über Anweisungen bzw. Vorlagen klarstellen, was Sie von den Studierenden erwarten.
CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben CourseApplicationExists: Sie haben sich bereits für diesen Kurs beworben
CourseApplicationInvalidAction: Angegebene Aktion kann nicht durchgeführt werden CourseApplicationInvalidAction: Angegebene Aktion kann nicht durchgeführt werden
CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben CourseApplicationCreated csh@CourseShorthand: Erfolgreich zu #{csh} beworben
@ -254,6 +258,8 @@ CourseRegistrationFilesNeedReupload: Dateien zur Anmeldung müssen neu hochgelad
CourseApplicationDeleteToEdit: Um Ihre Bewerbung zu editieren müssen Sie sie zunächst zurückziehen und sich erneut bewerben. CourseApplicationDeleteToEdit: Um Ihre Bewerbung zu editieren müssen Sie sie zunächst zurückziehen und sich erneut bewerben.
CourseRegistrationDeleteToEdit: Um Ihre Anmeldungsdaten zu editieren müssen Sie sich zunächst ab- und dann erneut anmelden. CourseRegistrationDeleteToEdit: Um Ihre Anmeldungsdaten zu editieren müssen Sie sich zunächst ab- und dann erneut anmelden.
CourseDeregistrationNoReRegistration: Wenn Sie sich jetzt vom Kurs abmelden, können Sie sich nicht wieder selbstständig anmelden.
CourseLoginToApply: Um sich zum Kurz zu bewerben müssen Sie sich zunächst in Uni2work anmelden CourseLoginToApply: Um sich zum Kurz zu bewerben müssen Sie sich zunächst in Uni2work anmelden
CourseLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2work anmelden CourseLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2work anmelden
@ -787,6 +793,12 @@ AccessRightsNotChanged: Berechtigungen wurden nicht verändert
UserSystemFunctions: Systemweite Rollen UserSystemFunctions: Systemweite Rollen
UserSystemFunctionsSaved: Systemweite Rollen gespeichert UserSystemFunctionsSaved: Systemweite Rollen gespeichert
UserSystemFunctionsNotChanged: Es wurden keine systemweiten Rollen angepasst UserSystemFunctionsNotChanged: Es wurden keine systemweiten Rollen angepasst
UserAssimilateUser: Benutzer
BtnUserAssimilate: Assimilieren
AssimilateUserNotFound: E-Mail Adresse konnte keinem Benutzer zugeordnet werden
AssimilateUserHaveError: Beim Assimilieren ist ein Fehler aufgetreten
AssimilateUserHaveWarnings: Beim Assimilieren wurden Warnungen ausgegeben
AssimilateUserSuccess: Benutzer erfolgreich assimiliert
LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"} LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"}
@ -2023,6 +2035,7 @@ ExamUsersResultsReset count@Int64: Prüfungsergebnis für #{show count} Teilnehm
ExamUsersPartResultsSet count@Int64: Teilprüfungsergebnis für #{show count} Teilnehmer angepasst ExamUsersPartResultsSet count@Int64: Teilprüfungsergebnis für #{show count} Teilnehmer angepasst
ExamUsersBonusSet count@Int64: Bonuspunkte für #{show count} Teilnehmer angepasst ExamUsersBonusSet count@Int64: Bonuspunkte für #{show count} Teilnehmer angepasst
ExamUsersResultSet count@Int64: Prüfungsergebnis für #{show count} Teilnehmer angepasst ExamUsersResultSet count@Int64: Prüfungsergebnis für #{show count} Teilnehmer angepasst
ExamUsersExamDataRequiresRegistration: Wenn Prüfungsbezogene Daten (Teil-/Ergebnis, Termin/Raum, Bonus) gesetzt bzw. angepasst werden sollen, muss der jeweilige Teilnehmer zur Prüfung angemeldet sein bzw. werden.
CourseUserTutorialsDeregistered count@Int64: Teilnehmer von #{show count} #{pluralDE count "Tutorium" "Tutorien"} abgemeldet CourseUserTutorialsDeregistered count@Int64: Teilnehmer von #{show count} #{pluralDE count "Tutorium" "Tutorien"} abgemeldet
CourseUserNoTutorialsDeregistered: Teilnehmer ist zu keinem der gewählten Tutorien angemeldet CourseUserNoTutorialsDeregistered: Teilnehmer ist zu keinem der gewählten Tutorien angemeldet
CourseUserExamsDeregistered count@Int64: Teilnehmer von #{show count} #{pluralDE count "Prüfung" "Prüfungen"} abgemeldet CourseUserExamsDeregistered count@Int64: Teilnehmer von #{show count} #{pluralDE count "Prüfung" "Prüfungen"} abgemeldet
@ -2348,6 +2361,8 @@ UserDisplayEmail: Angezeigte E-Mail-Adresse
UserDisplayEmailTip: Diese Adresse wird in öffentlich zugänglichen Teilen des Systems im Zusammenhang mit Ihrem Namen angezeigt. Benachrichtigungen und andere Kommunikation von Uni2work und Nutzern mit erweiterten Rechten erhalten sie stets, unabhängig von dieser Einstellung, an die in Ihren Persönlichen Daten hinterlegte primäre Adresse. UserDisplayEmailTip: Diese Adresse wird in öffentlich zugänglichen Teilen des Systems im Zusammenhang mit Ihrem Namen angezeigt. Benachrichtigungen und andere Kommunikation von Uni2work und Nutzern mit erweiterten Rechten erhalten sie stets, unabhängig von dieser Einstellung, an die in Ihren Persönlichen Daten hinterlegte primäre Adresse.
UserDisplayEmailChangeSent displayEmail@UserEmail: Anweisungen zum Ändern der angezeigten E-Mail-Adresse wurden an „#{displayEmail}” versandt UserDisplayEmailChangeSent displayEmail@UserEmail: Anweisungen zum Ändern der angezeigten E-Mail-Adresse wurden an „#{displayEmail}” versandt
UserCreated: Account erstellt
SchoolShort: Kürzel SchoolShort: Kürzel
SchoolName: Name SchoolName: Name
SchoolLdapOrganisations: Assoziierte LDAP-Fragmente SchoolLdapOrganisations: Assoziierte LDAP-Fragmente
@ -2455,6 +2470,7 @@ AdminUserIdent: Identifikation
AdminUserAuth: Authentifizierung AdminUserAuth: Authentifizierung
AdminUserMatriculation: Matrikelnummer AdminUserMatriculation: Matrikelnummer
AdminUserSex: Geschlecht AdminUserSex: Geschlecht
AdminUserAssimilate: Benutzer assimilieren
AuthKindLDAP: Campus-Kennung AuthKindLDAP: Campus-Kennung
AuthKindPWHash: Uni2work-Kennung AuthKindPWHash: Uni2work-Kennung
UserAdded: Benutzer erfolgreich angelegt UserAdded: Benutzer erfolgreich angelegt

View File

@ -212,7 +212,9 @@ CourseApplicationInstructionsTip: Will be shown to students if they decide to ap
CourseApplicationTemplate: Application template CourseApplicationTemplate: Application template
CourseApplicationTemplateTip: Students can download this template if they decide to apply for this course CourseApplicationTemplateTip: Students can download this template if they decide to apply for this course
CourseApplicationsText: Text application CourseApplicationsText: Text application
CourseApplicationsTextTip: Should students submit a plaintext application (in addition to submitted files if applicable)? CourseApplicationsTextTip: Should students submit plaintext with their application/registration (in addition to submitted files if applicable)?
CourseApplicationsFiles: Application files
CourseApplicationsFilesTip: Should students submit files with their application/registration (in addition to plaintext if applicable)?
CourseApplicationRatingsVisible: Feedback to applications CourseApplicationRatingsVisible: Feedback to applications
CourseApplicationRatingsVisibleTip: Should students be allowed to view rating and comments on their application after the rating period? CourseApplicationRatingsVisibleTip: Should students be allowed to view rating and comments on their application after the rating period?
CourseApplicationRequired: Applications required CourseApplicationRequired: Applications required
@ -225,6 +227,8 @@ CourseApplicationTemplateArchiveName tid ssh csh: #{foldCase (termToText (unTerm
CourseApplication: Application CourseApplication: Application
CourseApplicationIsParticipant: Course participant CourseApplicationIsParticipant: Course participant
CourseApplicationInstructionsRecommended: Students can, as per the current course settings, submit files and/or texts with their applications/registrations. There are, however, no instructions for application or template files. You should either not require files/texts or clarify through instructions or templates what is expected of the students.
CourseApplicationExists: You already applied for this course CourseApplicationExists: You already applied for this course
CourseApplicationInvalidAction: Invalid action CourseApplicationInvalidAction: Invalid action
CourseApplicationCreated csh: Successfully applied for #{csh} CourseApplicationCreated csh: Successfully applied for #{csh}
@ -254,6 +258,8 @@ CourseRegistrationFilesNeedReupload: Registration files need to be reuploaded ev
CourseApplicationDeleteToEdit: You need to withdraw your application and reapply to edit your application. CourseApplicationDeleteToEdit: You need to withdraw your application and reapply to edit your application.
CourseRegistrationDeleteToEdit: You need to deregister and reregister to edit your registration. CourseRegistrationDeleteToEdit: You need to deregister and reregister to edit your registration.
CourseDeregistrationNoReRegistration: If you deregister from the course now, you will not be able to re-register yourself.
CourseLoginToApply: You need to login to Uni2work before you can apply for this course. CourseLoginToApply: You need to login to Uni2work before you can apply for this course.
CourseLoginToRegister: Your need to login to Uni2work before you can register for this course. CourseLoginToRegister: Your need to login to Uni2work before you can register for this course.
@ -772,6 +778,12 @@ AccessRightsNotChanged: Permissions left unchanged
UserSystemFunctions: System wide roles UserSystemFunctions: System wide roles
UserSystemFunctionsSaved: Successfully saved system wide roles UserSystemFunctionsSaved: Successfully saved system wide roles
UserSystemFunctionsNotChanged: No system wide roles were changed UserSystemFunctionsNotChanged: No system wide roles were changed
UserAssimilateUser: User
BtnUserAssimilate: Assimilate
AssimilateUserNotFound: Email could not be resolved to an user
AssimilateUserHaveError: An error occurred during assimilation
AssimilateUserHaveWarnings: Warnings were ermitted during assimilation
AssimilateUserSuccess: Successfully assimilated user
LecturersForN n: #{pluralEN n "Lecturer" "Lecturers"} LecturersForN n: #{pluralEN n "Lecturer" "Lecturers"}
@ -1982,6 +1994,7 @@ ExamUsersResultsReset count: Successfully reset result for #{show count} #{plura
ExamUsersPartResultsSet count: Successfully modified exam part result for #{show count} #{pluralEN count "participant" "participants"} ExamUsersPartResultsSet count: Successfully modified exam part result for #{show count} #{pluralEN count "participant" "participants"}
ExamUsersBonusSet count: Successfully modified exam bonus for #{show count} #{pluralEN count "participant" "participants"} ExamUsersBonusSet count: Successfully modified exam bonus for #{show count} #{pluralEN count "participant" "participants"}
ExamUsersResultSet count: Sucessfully modified exam result for #{show count} #{pluralEN count "participant" "participants"} ExamUsersResultSet count: Sucessfully modified exam result for #{show count} #{pluralEN count "participant" "participants"}
ExamUsersExamDataRequiresRegistration: If exam data (part-/result, occurrence/room, bonus) is to be modified/set, the relenvant participant needs to be registered for the exam.
CourseUserTutorialsDeregistered count: Sucessfully deregistered participant from #{show count} #{pluralEN count "tutorial" "tutorials"} CourseUserTutorialsDeregistered count: Sucessfully deregistered participant from #{show count} #{pluralEN count "tutorial" "tutorials"}
CourseUserNoTutorialsDeregistered: Participant is not registered for any of the selected tutorials CourseUserNoTutorialsDeregistered: Participant is not registered for any of the selected tutorials
CourseUserExamsDeregistered count: Successfully deregistered participant from #{show count} #{pluralEN count "exam" "exams"} CourseUserExamsDeregistered count: Successfully deregistered participant from #{show count} #{pluralEN count "exam" "exams"}
@ -2308,6 +2321,8 @@ UserDisplayEmail: Display email
UserDisplayEmailTip: This email address may be displayed publicly alongside your display name. Notifications and other communication from Uni2work or users with elevated permissions are always sent to your primary email address as specified under "personal information". UserDisplayEmailTip: This email address may be displayed publicly alongside your display name. Notifications and other communication from Uni2work or users with elevated permissions are always sent to your primary email address as specified under "personal information".
UserDisplayEmailChangeSent displayEmail: Instructions to change your display email have been sent to “#{displayEmail}”. UserDisplayEmailChangeSent displayEmail: Instructions to change your display email have been sent to “#{displayEmail}”.
UserCreated: Account created
SchoolShort: Shorthand SchoolShort: Shorthand
SchoolName: Name SchoolName: Name
SchoolLdapOrganisations: Associated LDAP fragments SchoolLdapOrganisations: Associated LDAP fragments
@ -2415,6 +2430,7 @@ AdminUserIdent: Identification
AdminUserAuth: Authentication AdminUserAuth: Authentication
AdminUserMatriculation: Matriculation AdminUserMatriculation: Matriculation
AdminUserSex: Sex AdminUserSex: Sex
AdminUserAssimilate: Assimilate user
AuthKindLDAP: Campus account AuthKindLDAP: Campus account
AuthKindPWHash: Uni2work account AuthKindPWHash: Uni2work account
UserAdded: Successfully added user UserAdded: Successfully added user

View File

@ -45,12 +45,14 @@ AllocationUser
totalCourses Natural -- number of total allocated courses for this user must be <= than this number totalCourses Natural -- number of total allocated courses for this user must be <= than this number
priority AllocationPriority Maybe priority AllocationPriority Maybe
UniqueAllocationUser allocation user UniqueAllocationUser allocation user
deriving Eq Ord Show
AllocationDeregister -- self-inflicted user-deregistrations from an allocated course AllocationDeregister -- self-inflicted user-deregistrations from an allocated course
user UserId user UserId
course CourseId Maybe course CourseId Maybe
time UTCTime time UTCTime
reason Text Maybe -- if this deregistration was done by proxy (e.g. the lecturer pressed the button) reason Text Maybe -- if this deregistration was done by proxy (e.g. the lecturer pressed the button)
deriving Eq Ord Show
AllocationNotificationSetting AllocationNotificationSetting
user UserId user UserId

View File

@ -60,6 +60,7 @@ CourseParticipant -- course enrolement
allocated AllocationId Maybe -- participant was centrally allocated allocated AllocationId Maybe -- participant was centrally allocated
state CourseParticipantState state CourseParticipantState
UniqueParticipant user course UniqueParticipant user course
deriving Eq Ord Show
-- Replace the last two by the following, once an audit log is available -- Replace the last two by the following, once an audit log is available
-- CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student -- CourseUserNote -- lecturers of a specific course may share a text note on each enrolled student
-- course CourseId -- course CourseId

View File

@ -43,24 +43,28 @@ ExamRegistration
occurrence ExamOccurrenceId Maybe occurrence ExamOccurrenceId Maybe
time UTCTime default=now() time UTCTime default=now()
UniqueExamRegistration exam user UniqueExamRegistration exam user
deriving Eq Ord Show
ExamPartResult ExamPartResult
examPart ExamPartId examPart ExamPartId
user UserId user UserId
result ExamResultPoints result ExamResultPoints
lastChanged UTCTime default=now() lastChanged UTCTime default=now()
UniqueExamPartResult examPart user UniqueExamPartResult examPart user
deriving Eq Ord Show
ExamBonus ExamBonus
exam ExamId exam ExamId
user UserId user UserId
bonus Points bonus Points
lastChanged UTCTime default=now() lastChanged UTCTime default=now()
UniqueExamBonus exam user UniqueExamBonus exam user
deriving Eq Ord Show
ExamResult ExamResult
exam ExamId exam ExamId
user UserId user UserId
result ExamResultPassedGrade result ExamResultPassedGrade
lastChanged UTCTime default=now() lastChanged UTCTime default=now()
UniqueExamResult exam user UniqueExamResult exam user
deriving Eq Ord Show
ExamCorrector ExamCorrector
exam ExamId exam ExamId
user UserId user UserId

View File

@ -13,6 +13,7 @@ ExternalExamResult
time UTCTime time UTCTime
lastChanged UTCTime lastChanged UTCTime
UniqueExternalExamResult exam user UniqueExternalExamResult exam user
deriving Eq Ord Show
ExternalExamStaff ExternalExamStaff
user UserId user UserId
exam ExternalExamId exam ExternalExamId

View File

@ -17,14 +17,6 @@ CronLastExec
instance InstanceId -- Which uni2work-instance did the work instance InstanceId -- Which uni2work-instance did the work
UniqueCronLastExec job UniqueCronLastExec job
SentNotification
content Value
user UserId
time UTCTime
instance InstanceId
TokenBucket TokenBucket
ident TokenBucketIdent ident TokenBucketIdent
lastValue Int64 lastValue Int64

13
models/mail.model Normal file
View File

@ -0,0 +1,13 @@
SentMail
sentAt UTCTime
sentBy InstanceId
objectId MailObjectId Maybe
bounceSecret BounceSecret Maybe
recipient UserId Maybe
headers MailHeaders
contentRef SentMailContentId
SentMailContent
hash MailContentReference
content MailContent
Primary hash

View File

@ -31,3 +31,4 @@ SubmissionGroupUser -- Registered submission groups, just for check
submissionGroup SubmissionGroupId submissionGroup SubmissionGroupId
user UserId user UserId
UniqueSubmissionGroupUser submissionGroup user UniqueSubmissionGroupUser submissionGroup user
deriving Eq Ord Show

View File

@ -20,4 +20,5 @@ Tutor
TutorialParticipant TutorialParticipant
tutorial TutorialId tutorial TutorialId
user UserId user UserId
UniqueTutorialParticipant tutorial user UniqueTutorialParticipant tutorial user
deriving Eq Ord Show

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "20.13.0", "version": "21.0.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "uni2work", "name": "uni2work",
"version": "20.13.0", "version": "21.0.3",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 20.13.0 version: 21.0.3
dependencies: dependencies:
- base - base

View File

@ -183,7 +183,7 @@ makeFoundation appSettings'@AppSettings{..} = do
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function -- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache = UniWorX {..} let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret = UniWorX {..}
-- The UniWorX {..} syntax is an example of record wild cards. For more -- The UniWorX {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
@ -199,6 +199,7 @@ makeFoundation appSettings'@AppSettings{..} = do
(error "ClusterID forced in tempFoundation") (error "ClusterID forced in tempFoundation")
(error "memcached forced in tempFoundation") (error "memcached forced in tempFoundation")
(error "MinioConn forced in tempFoundation") (error "MinioConn forced in tempFoundation")
(error "VerpSecret forced in tempFoundation")
runAppLoggingT tempFoundation $ do runAppLoggingT tempFoundation $ do
$logInfoS "InstanceID" $ UUID.toText appInstanceID $logInfoS "InstanceID" $ UUID.toText appInstanceID
@ -242,6 +243,7 @@ makeFoundation appSettings'@AppSettings{..} = do
appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool
appJSONWebKeySet <- clusterSetting (Proxy :: Proxy 'ClusterJSONWebKeySet) `runSqlPool` sqlPool appJSONWebKeySet <- clusterSetting (Proxy :: Proxy 'ClusterJSONWebKeySet) `runSqlPool` sqlPool
appClusterID <- clusterSetting (Proxy :: Proxy 'ClusterId) `runSqlPool` sqlPool appClusterID <- clusterSetting (Proxy :: Proxy 'ClusterId) `runSqlPool` sqlPool
appVerpSecret <- clusterSetting (Proxy :: Proxy 'ClusterVerpSecret) `runSqlPool` sqlPool
appMemcached <- for appMemcachedConf $ \memcachedConf -> do appMemcached <- for appMemcachedConf $ \memcachedConf -> do
$logDebugS "setup" "Memcached" $logDebugS "setup" "Memcached"
@ -259,7 +261,7 @@ makeFoundation appSettings'@AppSettings{..} = do
handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadCacheBucket Nothing handleIf isBucketExists (const $ return ()) $ Minio.makeBucket appUploadCacheBucket Nothing
return conn return conn
let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionStore appSecretBoxKey appWidgetMemcached appJSONWebKeySet appClusterID appMemcached appUploadCache appVerpSecret
-- Return the foundation -- Return the foundation
$logDebugS "setup" "Done" $logDebugS "setup" "Done"

View File

@ -170,6 +170,11 @@ data Transaction
, transactionUser :: UserId , transactionUser :: UserId
} }
| TransactionUserAssimilated
{ transactionUser :: UserId
, transactionAssimilatedUser :: UserId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions deriveJSON defaultOptions

View File

@ -4,7 +4,7 @@
module Database.Esqueleto.Utils module Database.Esqueleto.Utils
( true, false ( true, false
, justVal, justValList , justVal, justValList
, isJust , isJust, alt
, isInfixOf, hasInfix , isInfixOf, hasInfix
, strConcat, substring , strConcat, substring
, or, and , or, and
@ -30,6 +30,7 @@ module Database.Esqueleto.Utils
, selectCountRows , selectCountRows
, selectMaybe , selectMaybe
, day, diffDays , day, diffDays
, exprLift
, module Database.Esqueleto.Utils.TH , module Database.Esqueleto.Utils.TH
) where ) where
@ -83,6 +84,9 @@ justValList = E.valList . map Just
isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool) isJust :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value Bool)
isJust = E.not_ . E.isNothing isJust = E.not_ . E.isNothing
alt :: PersistField typ => E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value (Maybe typ)) -> E.SqlExpr (E.Value (Maybe typ))
alt a b = E.case_ [(isJust a, a), (isJust b, b)] b
infix 4 `isInfixOf`, `hasInfix` infix 4 `isInfixOf`, `hasInfix`
-- | Check if the first string is contained in the text derived from the second argument -- | Check if the first string is contained in the text derived from the second argument
@ -388,3 +392,29 @@ infixl 6 `diffDays`
diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int) diffDays :: E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Day) -> E.SqlExpr (E.Value Int)
-- ^ PostgreSQL is weird. -- ^ PostgreSQL is weird.
diffDays a b = E.veryUnsafeCoerceSqlExprValue $ a E.-. b diffDays a b = E.veryUnsafeCoerceSqlExprValue $ a E.-. b
class ExprLift e a | e -> a where
exprLift :: a -> e
instance PersistField a => ExprLift (E.SqlExpr (E.Value a)) a where
exprLift = E.val
instance (PersistField a, PersistField b, Finite a) => ExprLift (E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value b)) (a -> b) where
exprLift f v = E.case_
[ E.when_ (v E.==. E.val v') E.then_ (E.val $ f v')
| v' <- universeF
]
(E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ()))))
instance (PersistField a1, PersistField a2, PersistField b, Finite a1, Finite a2) => ExprLift (E.SqlExpr (E.Value a1) -> E.SqlExpr (E.Value a2) -> E.SqlExpr (E.Value b)) (a1 -> a2 -> b) where
exprLift f v1 v2 = E.case_
[ E.when_ ( v1 E.==. E.val v1'
E.&&. v2 E.==. E.val v2'
)
E.then_ (E.val $ f v1' v2')
| v1' <- universeF
, v2' <- universeF
]
(E.else_ $ E.else_ $ E.veryUnsafeCoerceSqlExprValue (E.nothing :: E.SqlExpr (E.Value (Maybe ()))))

View File

@ -46,7 +46,6 @@ import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import Control.Monad.Error.Class (MonadError(..)) import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Memo.Class (MonadMemo(..), for4) import Control.Monad.Memo.Class (MonadMemo(..), for4)
import Data.Aeson.Lens hiding (_Value, key) import Data.Aeson.Lens hiding (_Value, key)
@ -82,25 +81,27 @@ newtype InvalidAuthTag = InvalidAuthTag Text
instance Exception InvalidAuthTag instance Exception InvalidAuthTag
type AuthTagsEval m = AuthDNF -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> WriterT (Set AuthTag) m AuthResult
data AccessPredicate data AccessPredicate
= APPure (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Reader MsgRenderer AuthResult) = APPure (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> Reader MsgRenderer AuthResult)
| APHandler (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX AuthResult) | APHandler (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> HandlerFor UniWorX AuthResult)
| APDB (Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT SqlReadBackend (HandlerFor UniWorX) AuthResult) | APDB ((forall m. MonadAP m => AuthTagsEval m) -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> ReaderT SqlReadBackend (HandlerFor UniWorX) AuthResult)
class (MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => MonadAP m where class (MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => MonadAP m where
evalAccessPred :: AccessPredicate -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult evalAccessPred :: AccessPredicate -> (forall m'. MonadAP m' => AuthTagsEval m') -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
instance {-# INCOHERENT #-} (MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => MonadAP m where instance {-# INCOHERENT #-} (MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => MonadAP m where
evalAccessPred aPred aid r w = liftHandler $ case aPred of evalAccessPred aPred cont aid r w = liftHandler $ case aPred of
(APPure p) -> runReader (p aid r w) <$> getMsgRenderer (APPure p) -> runReader (p aid r w) <$> getMsgRenderer
(APHandler p) -> p aid r w (APHandler p) -> p aid r w
(APDB p) -> runDBRead $ p aid r w (APDB p) -> runDBRead $ p cont aid r w
instance (MonadHandler m, HandlerSite m ~ UniWorX, BackendCompatible SqlReadBackend backend, BearerAuthSite UniWorX) => MonadAP (ReaderT backend m) where instance (MonadHandler m, HandlerSite m ~ UniWorX, BackendCompatible SqlReadBackend backend, BearerAuthSite UniWorX) => MonadAP (ReaderT backend m) where
evalAccessPred aPred aid r w = mapReaderT liftHandler . withReaderT (projectBackend @SqlReadBackend) $ case aPred of evalAccessPred aPred cont aid r w = mapReaderT liftHandler . withReaderT (projectBackend @SqlReadBackend) $ case aPred of
(APPure p) -> lift $ runReader (p aid r w) <$> getMsgRenderer (APPure p) -> lift $ runReader (p aid r w) <$> getMsgRenderer
(APHandler p) -> lift $ p aid r w (APHandler p) -> lift $ p aid r w
(APDB p) -> p aid r w (APDB p) -> p cont aid r w
orAR, andAR :: MsgRenderer -> AuthResult -> AuthResult -> AuthResult orAR, andAR :: MsgRenderer -> AuthResult -> AuthResult -> AuthResult
@ -178,7 +179,9 @@ isDryRun = $cachedHere . liftHandler $ orM
noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar
dnf <- either throwM return $ routeAuthTags currentRoute dnf <- either throwM return $ routeAuthTags currentRoute
guardAuthResult <=< fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) (noTokenAuth dnf) mAuthId currentRoute isWrite let eval :: forall m'. MonadAP m' => AuthTagsEval m'
eval dnf' mAuthId' route' isWrite' = evalAuthTags 'isDryRun (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId' route' isWrite'
in guardAuthResult <=< fmap fst . runWriterT $ eval dnf mAuthId currentRoute isWrite
return False return False
@ -230,6 +233,8 @@ validateBearer mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo val
noTokenAuth :: AuthDNF -> AuthDNF noTokenAuth :: AuthDNF -> AuthDNF
noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar noTokenAuth = over _dnfTerms . Set.filter . noneOf (re _nullable . folded) $ (== AuthToken) . plVar
eval :: forall m'. MonadAP m' => AuthTagsEval m'
eval dnf' mAuthId'' route'' isWrite'' = evalAuthTags 'validateBearer (AuthTagActive $ const True) eval (noTokenAuth dnf') mAuthId'' route'' isWrite''
guardMExceptT (not $ Set.null bearerAuthority') $ unauthorizedI MsgUnauthorizedTokenInvalidNoAuthority guardMExceptT (not $ Set.null bearerAuthority') $ unauthorizedI MsgUnauthorizedTokenInvalidNoAuthority
forM_ bearerAuthority' $ \uid -> do forM_ bearerAuthority' $ \uid -> do
@ -238,12 +243,12 @@ validateBearer mAuthId' route' isWrite' token' = $runCachedMemoT $ for4 memo val
authorityVal <- do authorityVal <- do
dnf <- either throwM return $ routeAuthTags route dnf <- either throwM return $ routeAuthTags route
fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) (noTokenAuth dnf) (Just uid) route isWrite fmap fst . runWriterT $ eval (noTokenAuth dnf) (Just uid) route isWrite
guardExceptT (is _Authorized authorityVal) authorityVal guardExceptT (is _Authorized authorityVal) authorityVal
whenIsJust bearerAddAuth $ \addDNF -> do whenIsJust bearerAddAuth $ \addDNF -> do
$logDebugS "validateToken" $ tshow addDNF $logDebugS "validateToken" $ tshow addDNF
additionalVal <- fmap fst . runWriterT $ evalAuthTags (AuthTagActive $ const True) (noTokenAuth addDNF) mAuthId route isWrite additionalVal <- fmap fst . runWriterT $ eval (noTokenAuth addDNF) mAuthId route isWrite
guardExceptT (is _Authorized additionalVal) additionalVal guardExceptT (is _Authorized additionalVal) additionalVal
return Authorized return Authorized
@ -295,7 +300,7 @@ maybeCurrentBearerRestrictions = liftHandler . runMaybeT $ do
tagAccessPredicate :: BearerAuthSite UniWorX tagAccessPredicate :: BearerAuthSite UniWorX
=> AuthTag -> AccessPredicate => AuthTag -> AccessPredicate
tagAccessPredicate AuthFree = trueAP tagAccessPredicate AuthFree = trueAP
tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthAdmin = APDB $ \_ mAuthId route _ -> case route of
-- Courses: access only to school admins -- Courses: access only to school admins
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
@ -332,12 +337,12 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of
adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] [] adrights <- lift $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAdmin] []
guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin) guardMExceptT (isJust adrights) (unauthorizedI MsgUnauthorizedSiteAdmin)
return Authorized return Authorized
tagAccessPredicate AuthSystemExamOffice = APDB $ \mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do tagAccessPredicate AuthSystemExamOffice = APDB $ \_ mAuthId _ _ -> $cachedHereBinary mAuthId . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isExamOffice <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemExamOffice, UserSystemFunctionIsOptOut ==. False] isExamOffice <- lift $ exists [UserSystemFunctionUser ==. authId, UserSystemFunctionFunction ==. SystemExamOffice, UserSystemFunctionIsOptOut ==. False]
guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedSystemExamOffice guardMExceptT isExamOffice $ unauthorizedI MsgUnauthorizedSystemExamOffice
return Authorized return Authorized
tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamOffice = APDB $ \_ mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasUsers <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do hasUsers <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
@ -375,7 +380,7 @@ tagAccessPredicate AuthExamOffice = APDB $ \mAuthId route _ -> case route of
isExamOffice <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice] isExamOffice <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolExamOffice]
guardMExceptT isExamOffice (unauthorizedI MsgUnauthorizedExamOffice) guardMExceptT isExamOffice (unauthorizedI MsgUnauthorizedExamOffice)
return Authorized return Authorized
tagAccessPredicate AuthEvaluation = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthEvaluation = APDB $ \_ mAuthId route _ -> case route of
ParticipantsR _ ssh -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do ParticipantsR _ ssh -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolEvaluation isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolEvaluation
@ -391,7 +396,7 @@ tagAccessPredicate AuthEvaluation = APDB $ \mAuthId route _ -> case route of
isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolEvaluation] isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolEvaluation]
guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedEvaluation
return Authorized return Authorized
tagAccessPredicate AuthAllocationAdmin = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthAllocationAdmin = APDB $ \_ mAuthId route _ -> case route of
AllocationR _ ssh _ _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do AllocationR _ ssh _ _ -> $cachedHereBinary (mAuthId, ssh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolAllocation isEvaluation <- lift . existsBy $ UniqueUserFunction authId ssh SchoolAllocation
@ -407,9 +412,9 @@ tagAccessPredicate AuthAllocationAdmin = APDB $ \mAuthId route _ -> case route o
isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAllocation] isEvaluation <- lift $ exists [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolAllocation]
guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedAllocationAdmin guardMExceptT isEvaluation $ unauthorizedI MsgUnauthorizedAllocationAdmin
return Authorized return Authorized
tagAccessPredicate AuthToken = APDB $ \mAuthId route isWrite -> exceptT return return $ tagAccessPredicate AuthToken = APDB $ \_ mAuthId route isWrite -> exceptT return return $
lift . validateBearer mAuthId route isWrite =<< askBearerUnsafe lift . validateBearer mAuthId route isWrite =<< askBearerUnsafe
tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthNoEscalation = APDB $ \_ mAuthId route _ -> case route of
AdminHijackUserR cID -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do AdminHijackUserR cID -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do
myUid <- maybeExceptT AuthenticationRequired $ return mAuthId myUid <- maybeExceptT AuthenticationRequired $ return mAuthId
uid <- decrypt cID uid <- decrypt cID
@ -430,7 +435,7 @@ tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
#else #else
return $ Unauthorized "Route under development" return $ Unauthorized "Route under development"
#endif #endif
tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthLecturer = APDB $ \_ mAuthId route _ -> case route of
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isLecturer <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` lecturer) -> do isLecturer <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` lecturer) -> do
@ -469,7 +474,7 @@ tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolLecturer] [] void . maybeMExceptT (unauthorizedI MsgUnauthorizedSchoolLecturer) $ selectFirst [UserFunctionUser ==. authId, UserFunctionFunction ==. SchoolLecturer] []
return Authorized return Authorized
tagAccessPredicate AuthCorrector = APDB $ \mAuthId route _ -> exceptT return return $ do tagAccessPredicate AuthCorrector = APDB $ \_ mAuthId route _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
resList <- $cachedHereBinary mAuthId . lift . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do resList <- $cachedHereBinary mAuthId . lift . E.select . E.from $ \(course `E.InnerJoin` sheet `E.InnerJoin` sheetCorrector) -> do
E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId E.on $ sheetCorrector E.^. SheetCorrectorSheet E.==. sheet E.^. SheetId
@ -497,7 +502,7 @@ tagAccessPredicate AuthCorrector = APDB $ \mAuthId route _ -> exceptT return ret
_ -> do _ -> do
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedCorrectorAny) guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedCorrectorAny)
return Authorized return Authorized
tagAccessPredicate AuthExamCorrector = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamCorrector = APDB $ \_ mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do CExamR tid ssh csh examn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, examn) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isCorrector <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do isCorrector <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examCorrector) -> do
@ -522,7 +527,7 @@ tagAccessPredicate AuthExamCorrector = APDB $ \mAuthId route _ -> case route of
guardMExceptT isCorrector $ unauthorizedI MsgUnauthorizedExamCorrector guardMExceptT isCorrector $ unauthorizedI MsgUnauthorizedExamCorrector
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthExamCorrector r r -> $unsupportedAuthPredicate AuthExamCorrector r
tagAccessPredicate AuthTutor = APDB $ \mAuthId route _ -> exceptT return return $ do tagAccessPredicate AuthTutor = APDB $ \_ mAuthId route _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
resList <- $cachedHereBinary authId . lift . E.select . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do resList <- $cachedHereBinary authId . lift . E.select . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutor) -> do
E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId E.on $ tutor E.^. TutorTutorial E.==. tutorial E.^. TutorialId
@ -545,14 +550,14 @@ tagAccessPredicate AuthTutor = APDB $ \mAuthId route _ -> exceptT return return
_ -> do _ -> do
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor) guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
return Authorized return Authorized
tagAccessPredicate AuthTutorControl = APDB $ \_ route _ -> case route of tagAccessPredicate AuthTutorControl = APDB $ \_ _ route _ -> case route of
CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialTutorControl) $ do CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialTutorControl) $ do
Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
guard tutorialTutorControlled guard tutorialTutorControlled
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTutorControl r r -> $unsupportedAuthPredicate AuthTutorControl r
tagAccessPredicate AuthSubmissionGroup = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthSubmissionGroup = APDB $ \_ mAuthId route _ -> case route of
CSubmissionR tid ssh csh shn cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionSubmissionGroup) $ do CSubmissionR tid ssh csh shn cID _ -> maybeT (unauthorizedI MsgUnauthorizedSubmissionSubmissionGroup) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity _ Sheet{..} <- $cachedHereBinary (course, shn) . MaybeT . getBy $ CourseSheet course shn Entity _ Sheet{..} <- $cachedHereBinary (course, shn) . MaybeT . getBy $ CourseSheet course shn
@ -577,7 +582,7 @@ tagAccessPredicate AuthSubmissionGroup = APDB $ \mAuthId route _ -> case route o
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthSubmissionGroup r r -> $unsupportedAuthPredicate AuthSubmissionGroup r
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthTime = APDB $ \(runTACont -> cont) mAuthId route isWrite -> case route of
CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn
@ -677,9 +682,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
CourseR tid ssh csh CRegisterR -> do CourseR tid ssh csh CRegisterR -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
mbc <- $cachedHereBinary (tid, ssh, csh) . getBy $ TermSchoolCourseShort tid ssh csh mbc <- $cachedHereBinary (tid, ssh, csh) . getBy $ TermSchoolCourseShort tid ssh csh
registered <- case (mbc,mAuthId) of registered <- cont (predDNFSingleton $ PLVariable AuthCourseRegistered) mAuthId route isWrite
(Just (Entity cid _), Just uid) -> $cachedHereBinary (uid, cid) $ exists [CourseParticipantUser ==. uid, CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive]
_ -> return False
case mbc of case mbc of
(Just (Entity _ Course{courseRegisterFrom, courseRegisterTo})) (Just (Entity _ Course{courseRegisterFrom, courseRegisterTo}))
| not registered | not registered
@ -759,7 +762,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTime r r -> $unsupportedAuthPredicate AuthTime r
tagAccessPredicate AuthStaffTime = APDB $ \_ route isWrite -> case route of tagAccessPredicate AuthStaffTime = APDB $ \_ _ route isWrite -> case route of
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
@ -784,16 +787,14 @@ tagAccessPredicate AuthStaffTime = APDB $ \_ route isWrite -> case route of
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthStaffTime r r -> $unsupportedAuthPredicate AuthStaffTime r
tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthAllocationTime = APDB $ \(runTACont -> cont) mAuthId route isWrite -> case route of
CourseR tid ssh csh CRegisterR -> do CourseR tid ssh csh CRegisterR -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
mba <- mbAllocation tid ssh csh mba <- mbAllocation tid ssh csh
case mba of case mba of
Nothing -> return Authorized Nothing -> return Authorized
Just (cid, Allocation{..}) -> do Just (_, Allocation{..}) -> do
registered <- case mAuthId of registered <- cont (predDNFSingleton $ PLVariable AuthCourseRegistered) mAuthId route isWrite
Just uid -> $cachedHereBinary (uid, cid) $ exists [ CourseParticipantUser ==. uid, CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantActive ]
_ -> return False
if if
| not registered | not registered
, NTop allocationRegisterByCourse >= NTop (Just now) , NTop allocationRegisterByCourse >= NTop (Just now)
@ -830,7 +831,7 @@ tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of
cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity _ AllocationCourse{..} <- MaybeT . getBy $ UniqueAllocationCourse cid Entity _ AllocationCourse{..} <- MaybeT . getBy $ UniqueAllocationCourse cid
(cid,) <$> MaybeT (get allocationCourseAllocation) (cid,) <$> MaybeT (get allocationCourseAllocation)
tagAccessPredicate AuthCourseTime = APDB $ \_mAuthId route _ -> case route of tagAccessPredicate AuthCourseTime = APDB $ \_ _mAuthId route _ -> case route of
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
courseVisible <- $cachedHereBinary (tid, ssh, csh) . lift . E.selectExists . E.from $ \course -> do courseVisible <- $cachedHereBinary (tid, ssh, csh) . lift . E.selectExists . E.from $ \course -> do
@ -841,7 +842,7 @@ tagAccessPredicate AuthCourseTime = APDB $ \_mAuthId route _ -> case route of
guardMExceptT courseVisible (unauthorizedI MsgUnauthorizedCourseTime) guardMExceptT courseVisible (unauthorizedI MsgUnauthorizedCourseTime)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCourseTime r r -> $unsupportedAuthPredicate AuthCourseTime r
tagAccessPredicate AuthCourseRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthCourseRegistered = APDB $ \_ mAuthId route _ -> case route of
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
isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
@ -854,7 +855,7 @@ tagAccessPredicate AuthCourseRegistered = APDB $ \mAuthId route _ -> case route
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCourseRegistered r r -> $unsupportedAuthPredicate AuthCourseRegistered r
tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthTutorialRegistered = APDB $ \_ mAuthId route _ -> case route of
CTutorialR tid ssh csh tutn _ -> exceptT return return $ do CTutorialR tid ssh csh tutn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
isRegistered <- $cachedHereBinary (authId, tid, ssh, csh, tutn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do isRegistered <- $cachedHereBinary (authId, tid, ssh, csh, tutn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
@ -879,7 +880,7 @@ tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case rout
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTutorialRegistered r r -> $unsupportedAuthPredicate AuthTutorialRegistered r
tagAccessPredicate AuthExamOccurrenceRegistration = APDB $ \_ route _ -> case route of tagAccessPredicate AuthExamOccurrenceRegistration = APDB $ \_ _ route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do CExamR tid ssh csh examn _ -> exceptT return return $ do
isOccurrenceRegistration <- $cachedHereBinary (tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam) -> do isOccurrenceRegistration <- $cachedHereBinary (tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam) -> do
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
@ -891,7 +892,7 @@ tagAccessPredicate AuthExamOccurrenceRegistration = APDB $ \_ route _ -> case ro
guardMExceptT isOccurrenceRegistration (unauthorizedI MsgUnauthorizedExamOccurrenceRegistration) guardMExceptT isOccurrenceRegistration (unauthorizedI MsgUnauthorizedExamOccurrenceRegistration)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistration r r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistration r
tagAccessPredicate AuthExamOccurrenceRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamOccurrenceRegistered = APDB $ \_ mAuthId route _ -> case route of
CExamR tid ssh csh examn (ERegisterOccR occn) -> exceptT return return $ do CExamR tid ssh csh examn (ERegisterOccR occn) -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn, occn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration `E.InnerJoin` examOccurrence) -> do hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn, occn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration `E.InnerJoin` examOccurrence) -> do
@ -932,7 +933,7 @@ tagAccessPredicate AuthExamOccurrenceRegistered = APDB $ \mAuthId route _ -> cas
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered) guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistered r r -> $unsupportedAuthPredicate AuthExamOccurrenceRegistered r
tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamRegistered = APDB $ \_ mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
@ -973,7 +974,7 @@ tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
guardMExceptT hasRegistration $ unauthorizedI MsgUnauthorizedRegisteredAnyExam guardMExceptT hasRegistration $ unauthorizedI MsgUnauthorizedRegisteredAnyExam
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthExamRegistered r r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthExamResult = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamResult = APDB $ \_ mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasResult <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do hasResult <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examResult) -> do
@ -1026,14 +1027,14 @@ tagAccessPredicate AuthExamResult = APDB $ \mAuthId route _ -> case route of
guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult) guardMExceptT (hasResult || hasPartResult) (unauthorizedI MsgUnauthorizedExamResult)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthExamRegistered r r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthAllocationRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthAllocationRegistered = APDB $ \_ mAuthId route _ -> case route of
AllocationR tid ssh ash _ -> maybeT (unauthorizedI MsgUnauthorizedAllocationRegistered) $ do AllocationR tid ssh ash _ -> maybeT (unauthorizedI MsgUnauthorizedAllocationRegistered) $ do
uid <- hoistMaybe mAuthId uid <- hoistMaybe mAuthId
aId <- MaybeT . $cachedHereBinary (tid, ssh, ash) . getKeyBy $ TermSchoolAllocationShort tid ssh ash aId <- MaybeT . $cachedHereBinary (tid, ssh, ash) . getKeyBy $ TermSchoolAllocationShort tid ssh ash
void . MaybeT . $cachedHereBinary (uid, aId) . getKeyBy $ UniqueAllocationUser aId uid void . MaybeT . $cachedHereBinary (uid, aId) . getKeyBy $ UniqueAllocationUser aId uid
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthAllocationRegistered r r -> $unsupportedAuthPredicate AuthAllocationRegistered r
tagAccessPredicate AuthParticipant = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthParticipant = APDB $ \_ mAuthId route _ -> case route of
CNewsR tid ssh csh cID _ -> maybeT (unauthorizedI MsgUnauthorizedParticipantSelf) $ do CNewsR tid ssh csh cID _ -> maybeT (unauthorizedI MsgUnauthorizedParticipantSelf) $ do
nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID nId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
CourseNews{courseNewsParticipantsOnly} <- $cachedHereBinary nId . MaybeT $ get nId CourseNews{courseNewsParticipantsOnly} <- $cachedHereBinary nId . MaybeT $ get nId
@ -1141,7 +1142,7 @@ tagAccessPredicate AuthParticipant = APDB $ \mAuthId route _ -> case route of
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
tagAccessPredicate AuthApplicant = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthApplicant = APDB $ \_ mAuthId route _ -> case route of
CourseR tid ssh csh (CUserR cID) -> maybeT (unauthorizedI MsgUnauthorizedApplicant) $ do CourseR tid ssh csh (CUserR cID) -> maybeT (unauthorizedI MsgUnauthorizedApplicant) $ do
uid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID uid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
isApplicant <- isCourseApplicant tid ssh csh uid isApplicant <- isCourseApplicant tid ssh csh uid
@ -1162,7 +1163,7 @@ tagAccessPredicate AuthApplicant = APDB $ \mAuthId route _ -> case route of
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
tagAccessPredicate AuthCapacity = APDB $ \_ route _ -> case route of tagAccessPredicate AuthCapacity = APDB $ \_ _ route _ -> case route of
CExamR tid ssh csh examn (ERegisterOccR occn) -> maybeT (unauthorizedI MsgExamOccurrenceNoCapacity) $ do CExamR tid ssh csh examn (ERegisterOccR occn) -> maybeT (unauthorizedI MsgExamOccurrenceNoCapacity) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
eid <- $cachedHereBinary (cid, examn) . MaybeT . getKeyBy $ UniqueExam cid examn eid <- $cachedHereBinary (cid, examn) . MaybeT . getKeyBy $ UniqueExam cid examn
@ -1182,7 +1183,7 @@ tagAccessPredicate AuthCapacity = APDB $ \_ route _ -> case route of
guard $ NTop courseCapacity > NTop (Just registered) guard $ NTop courseCapacity > NTop (Just registered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCapacity r r -> $unsupportedAuthPredicate AuthCapacity r
tagAccessPredicate AuthRegisterGroup = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthRegisterGroup = APDB $ \_ mAuthId route _ -> case route of
CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialRegisterGroup) $ do CTutorialR tid ssh csh tutn _ -> maybeT (unauthorizedI MsgUnauthorizedTutorialRegisterGroup) $ do
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn Entity _ Tutorial{..} <- $cachedHereBinary (cid, tutn) . MaybeT . getBy $ UniqueTutorial cid tutn
@ -1198,7 +1199,7 @@ 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 $ \mAuthId route _ -> do tagAccessPredicate AuthEmpty = APDB $ \_ mAuthId route _ -> do
let wInstances rScope = maybeT (unauthorizedI MsgUnauthorizedWorkflowInstancesNotEmpty) $ do let wInstances rScope = maybeT (unauthorizedI MsgUnauthorizedWorkflowInstancesNotEmpty) $ do
scope <- fromRouteWorkflowScope rScope scope <- fromRouteWorkflowScope rScope
@ -1247,20 +1248,20 @@ tagAccessPredicate AuthEmpty = APDB $ \mAuthId route _ -> do
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthEmpty r r -> $unsupportedAuthPredicate AuthEmpty r
tagAccessPredicate AuthMaterials = APDB $ \_ route _ -> case route of tagAccessPredicate AuthMaterials = APDB $ \_ _ route _ -> case route of
CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnfreeMaterials) $ do CourseR tid ssh csh _ -> maybeT (unauthorizedI MsgUnfreeMaterials) $ do
Entity _ Course{..} <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh Entity _ Course{..} <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
guard courseMaterialFree guard courseMaterialFree
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthMaterials r r -> $unsupportedAuthPredicate AuthMaterials r
tagAccessPredicate AuthOwner = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthOwner = APDB $ \_ mAuthId route _ -> case route of
CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary (mAuthId, cID) . exceptT return return $ do
sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID sid <- catchIfMExceptT (const $ unauthorizedI MsgUnauthorizedSubmissionOwner) (const True :: CryptoIDError -> Bool) $ decrypt cID
authId <- maybeExceptT AuthenticationRequired $ return mAuthId authId <- maybeExceptT AuthenticationRequired $ return mAuthId
void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid void . maybeMExceptT (unauthorizedI MsgUnauthorizedSubmissionOwner) . getBy $ UniqueSubmissionUser authId sid
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthOwner r r -> $unsupportedAuthPredicate AuthOwner r
tagAccessPredicate AuthPersonalisedSheetFiles = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthPersonalisedSheetFiles = APDB $ \_ mAuthId route _ -> case route of
CSheetR tid ssh csh shn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, shn) . exceptT return return $ do CSheetR tid ssh csh shn _ -> $cachedHereBinary (mAuthId, tid, ssh, csh, shn) . exceptT return return $ do
Entity shId Sheet{..} <- maybeTMExceptT (unauthorizedI MsgUnauthorizedSubmissionPersonalisedSheetFiles) $ do Entity shId Sheet{..} <- maybeTMExceptT (unauthorizedI MsgUnauthorizedSubmissionPersonalisedSheetFiles) $ do
cid <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh cid <- MaybeT . $cachedHereBinary (tid, ssh, csh) . getKeyBy $ TermSchoolCourseShort tid ssh csh
@ -1275,28 +1276,28 @@ tagAccessPredicate AuthPersonalisedSheetFiles = APDB $ \mAuthId route _ -> case
E.&&. E.not_ (E.isNothing $ psFile E.^. PersonalisedSheetFileContent) -- directories don't count E.&&. E.not_ (E.isNothing $ psFile E.^. PersonalisedSheetFileContent) -- directories don't count
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthPersonalisedSheetFiles r r -> $unsupportedAuthPredicate AuthPersonalisedSheetFiles r
tagAccessPredicate AuthRated = APDB $ \_ route _ -> case route of tagAccessPredicate AuthRated = APDB $ \_ _ route _ -> case route of
CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary cID . maybeT (unauthorizedI MsgUnauthorizedSubmissionRated) $ do CSubmissionR _ _ _ _ cID _ -> $cachedHereBinary cID . maybeT (unauthorizedI MsgUnauthorizedSubmissionRated) $ do
sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID sid <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
sub <- MaybeT $ get sid sub <- MaybeT $ get sid
guard $ submissionRatingDone sub guard $ submissionRatingDone sub
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthRated r r -> $unsupportedAuthPredicate AuthRated r
tagAccessPredicate AuthUserSubmissions = APDB $ \_ route _ -> case route of tagAccessPredicate AuthUserSubmissions = APDB $ \_ _ route _ -> case route of
CSheetR tid ssh csh shn _ -> $cachedHereBinary (tid, ssh, csh, shn) . maybeT (unauthorizedI MsgUnauthorizedUserSubmission) $ do CSheetR tid ssh csh shn _ -> $cachedHereBinary (tid, ssh, csh, shn) . maybeT (unauthorizedI MsgUnauthorizedUserSubmission) $ do
Entity cid _ <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh Entity cid _ <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- MaybeT . getBy $ CourseSheet cid shn Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- MaybeT . getBy $ CourseSheet cid shn
guard $ is _Just submissionModeUser guard $ is _Just submissionModeUser
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthUserSubmissions r r -> $unsupportedAuthPredicate AuthUserSubmissions r
tagAccessPredicate AuthCorrectorSubmissions = APDB $ \_ route _ -> case route of tagAccessPredicate AuthCorrectorSubmissions = APDB $ \_ _ route _ -> case route of
CSheetR tid ssh csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedCorrectorSubmission) $ do CSheetR tid ssh csh shn _ -> maybeT (unauthorizedI MsgUnauthorizedCorrectorSubmission) $ do
Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh Entity cid _ <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn Entity _ Sheet{ sheetSubmissionMode = SubmissionMode{..} } <- $cachedHereBinary (cid, shn) . MaybeT . getBy $ CourseSheet cid shn
guard submissionModeCorrector guard submissionModeCorrector
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCorrectorSubmissions r r -> $unsupportedAuthPredicate AuthCorrectorSubmissions r
tagAccessPredicate AuthSelf = APDB $ \mAuthId route _ -> exceptT return return $ do tagAccessPredicate AuthSelf = APDB $ \_ mAuthId route _ -> exceptT return return $ do
referencedUser' <- case route of referencedUser' <- case route of
AdminUserR cID -> return $ Left cID AdminUserR cID -> return $ Left cID
AdminUserDeleteR cID -> return $ Left cID AdminUserDeleteR cID -> return $ Left cID
@ -1317,7 +1318,7 @@ tagAccessPredicate AuthSelf = APDB $ \mAuthId route _ -> exceptT return return $
| uid == referencedUser -> return Authorized | uid == referencedUser -> return Authorized
Nothing -> return AuthenticationRequired Nothing -> return AuthenticationRequired
_other -> unauthorizedI MsgUnauthorizedSelf _other -> unauthorizedI MsgUnauthorizedSelf
tagAccessPredicate AuthIsLDAP = APDB $ \_ route _ -> exceptT return return $ do tagAccessPredicate AuthIsLDAP = APDB $ \_ _ route _ -> exceptT return return $ do
referencedUser <- case route of referencedUser <- case route of
AdminUserR cID -> return cID AdminUserR cID -> return cID
AdminUserDeleteR cID -> return cID AdminUserDeleteR cID -> return cID
@ -1331,7 +1332,7 @@ tagAccessPredicate AuthIsLDAP = APDB $ \_ route _ -> exceptT return return $ do
User{..} <- MaybeT $ get referencedUser' User{..} <- MaybeT $ get referencedUser'
guard $ userAuthentication == AuthLDAP guard $ userAuthentication == AuthLDAP
return Authorized return Authorized
tagAccessPredicate AuthIsPWHash = APDB $ \_ route _ -> exceptT return return $ do tagAccessPredicate AuthIsPWHash = APDB $ \_ _ route _ -> exceptT return return $ do
referencedUser <- case route of referencedUser <- case route of
AdminUserR cID -> return cID AdminUserR cID -> return cID
AdminUserDeleteR cID -> return cID AdminUserDeleteR cID -> return cID
@ -1345,7 +1346,7 @@ tagAccessPredicate AuthIsPWHash = APDB $ \_ route _ -> exceptT return return $ d
User{..} <- MaybeT $ get referencedUser' User{..} <- MaybeT $ get referencedUser'
guard $ is _AuthPWHash userAuthentication guard $ is _AuthPWHash userAuthentication
return Authorized return Authorized
tagAccessPredicate AuthAuthentication = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthAuthentication = APDB $ \_ mAuthId route _ -> case route of
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageAuth) $ do
smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID smId <- catchIfMaybeT (const True :: CryptoIDError -> Bool) $ decrypt cID
SystemMessage{..} <- $cachedHereBinary smId . MaybeT $ get smId SystemMessage{..} <- $cachedHereBinary smId . MaybeT $ get smId
@ -1359,7 +1360,7 @@ tagAccessPredicate AuthAuthentication = APDB $ \mAuthId route _ -> case route of
guard $ not systemMessageAuthenticatedOnly || isAuthenticated guard $ not systemMessageAuthenticatedOnly || isAuthenticated
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthAuthentication r r -> $unsupportedAuthPredicate AuthAuthentication r
tagAccessPredicate AuthWorkflow = APDB $ \mAuthId route isWrite -> do tagAccessPredicate AuthWorkflow = APDB $ \_ mAuthId route isWrite -> do
mr <- getMsgRenderer mr <- getMsgRenderer
let orAR', _andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult let orAR', _andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult
orAR' = shortCircuitM (is _Authorized) (orAR mr) orAR' = shortCircuitM (is _Authorized) (orAR mr)
@ -1454,6 +1455,11 @@ tagAccessPredicate AuthWrite = APPure $ \_ _ isWrite -> do
MsgRenderer mr <- ask MsgRenderer mr <- ask
return $ bool (Unauthorized $ mr MsgUnauthorized) Authorized isWrite return $ bool (Unauthorized $ mr MsgUnauthorized) Authorized isWrite
runTACont :: forall m. MonadAP m
=> (forall m'. MonadAP m' => AuthTagsEval m')
-> AuthDNF -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m Bool
runTACont cont dnf mAuthId route isWrite = is _Authorized . fst <$> runWriterT (cont dnf mAuthId route isWrite)
authTagSpecificity :: AuthTag -> AuthTag -> Ordering authTagSpecificity :: AuthTag -> AuthTag -> Ordering
-- ^ Heuristic for which `AuthTag`s to evaluate first -- ^ Heuristic for which `AuthTag`s to evaluate first
@ -1495,9 +1501,9 @@ routeAuthTags = fmap (PredDNF . Set.mapMonotonic impureNonNull) . ofoldM partiti
| otherwise | otherwise
= Left $ InvalidAuthTag t = Left $ InvalidAuthTag t
evalAuthTags :: forall m. MonadAP m => AuthTagActive -> AuthDNF -> Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> WriterT (Set AuthTag) m AuthResult evalAuthTags :: forall ctx m. (Binary ctx, MonadAP m) => ctx -> AuthTagActive -> (forall m'. MonadAP m' => AuthTagsEval m') -> AuthTagsEval m
-- ^ `tell`s disabled predicates, identified as pivots -- ^ `tell`s disabled predicates, identified as pivots
evalAuthTags AuthTagActive{..} (map (Set.toList . toNullable) . Set.toList . dnfTerms -> authDNF') mAuthId route isWrite evalAuthTags ctx AuthTagActive{..} cont (map (Set.toList . toNullable) . Set.toList . dnfTerms -> authDNF') mAuthId route isWrite
= do = do
mr <- getMsgRenderer mr <- getMsgRenderer
let let
@ -1507,11 +1513,11 @@ evalAuthTags AuthTagActive{..} (map (Set.toList . toNullable) . Set.toList . dnf
authTagIsInactive = not . authTagIsActive authTagIsInactive = not . authTagIsActive
evalAuthTag :: AuthTag -> WriterT (Set AuthTag) m AuthResult evalAuthTag :: AuthTag -> WriterT (Set AuthTag) m AuthResult
evalAuthTag authTag = lift . ($runCachedMemoT :: CachedMemoT (AuthTag, Maybe UserId, Route UniWorX, Bool) AuthResult m _ -> m _) $ for4 memo evalAccessPred' authTag mAuthId route isWrite evalAuthTag authTag = lift . ($runCachedMemoT :: CachedMemoT (ctx, AuthTag, Maybe UserId, Route UniWorX, Bool) AuthResult m _ -> m _) $ for5 memo (const evalAccessPred') ctx authTag mAuthId route isWrite
where where
evalAccessPred' authTag' mAuthId' route' isWrite' = lift $ do evalAccessPred' authTag' mAuthId' route' isWrite' = lift $ do
$logDebugS "evalAccessPred" $ tshow (authTag', mAuthId', route', isWrite') $logDebugS "evalAccessPred" $ tshow (authTag', mAuthId', route', isWrite')
evalAccessPred (tagAccessPredicate authTag') mAuthId' route' isWrite' evalAccessPred (tagAccessPredicate authTag') cont mAuthId' route' isWrite'
evalAuthLiteral :: AuthLiteral -> WriterT (Set AuthTag) m AuthResult evalAuthLiteral :: AuthLiteral -> WriterT (Set AuthTag) m AuthResult
evalAuthLiteral PLVariable{..} = evalAuthTag plVar evalAuthLiteral PLVariable{..} = evalAuthTag plVar
@ -1547,14 +1553,16 @@ evalAccessWithFor assumptions mAuthId route isWrite = do
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags | isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
| otherwise -> return . AuthTagActive $ const True | otherwise -> return . AuthTagActive $ const True
dnf <- either throwM return $ routeAuthTags route dnf <- either throwM return $ routeAuthTags route
let dnf' = ala Endo foldMap (map ((=<<) . uncurry dnfAssumeValue) assumptions) $ Just dnf let adjDNF = ala Endo foldMap (map ((=<<) . uncurry dnfAssumeValue) assumptions) . Just
case dnf' of evalAdj :: forall m'. MonadAP m' => AuthTagsEval m'
Nothing -> return Authorized evalAdj (adjDNF -> dnf') mAuthId' route' isWrite' = case dnf' of
Just dnf'' -> do Nothing -> return Authorized
(result, deactivated) <- runWriterT $ evalAuthTags tagActive dnf'' mAuthId route isWrite Just dnf'' -> evalAuthTags ('evalAccessWithFor, assumptions) tagActive evalAdj dnf'' mAuthId' route' isWrite'
when isSelf $ in do
tellSessionJson SessionInactiveAuthTags deactivated (result, deactivated) <- runWriterT $ evalAdj dnf mAuthId route isWrite
return result when isSelf $
tellSessionJson SessionInactiveAuthTags deactivated
return result
evalAccessFor :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult evalAccessFor :: (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX, BearerAuthSite UniWorX) => Maybe (AuthId UniWorX) -> Route UniWorX -> Bool -> m AuthResult
evalAccessFor = evalAccessWithFor [] evalAccessFor = evalAccessWithFor []
@ -1621,14 +1629,14 @@ evalWorkflowRoleFor' :: forall m backend.
, BearerAuthSite UniWorX , BearerAuthSite UniWorX
, BackendCompatible SqlReadBackend backend , BackendCompatible SqlReadBackend backend
) )
=> AuthTagActive => (forall m'. MonadAP m' => AuthTagsEval m')
-> Maybe UserId -> Maybe UserId
-> Maybe WorkflowWorkflowId -> Maybe WorkflowWorkflowId
-> WorkflowRole UserId -> WorkflowRole UserId
-> Route UniWorX -> Route UniWorX
-> Bool -> Bool
-> WriterT (Set AuthTag) (ReaderT backend m) AuthResult -> WriterT (Set AuthTag) (ReaderT backend m) AuthResult
evalWorkflowRoleFor' tagActive mAuthId mwwId wRole route isWrite = $cachedHereBinary (tagActive, mAuthId, mwwId, wRole, route, isWrite) $ case wRole of evalWorkflowRoleFor' eval mAuthId mwwId wRole route isWrite = case wRole of
WorkflowRoleUser{..} -> lift . exceptT return return $ do WorkflowRoleUser{..} -> lift . exceptT return return $ do
uid <- maybeExceptT AuthenticationRequired $ return mAuthId uid <- maybeExceptT AuthenticationRequired $ return mAuthId
unless (uid == workflowRoleUser) $ unless (uid == workflowRoleUser) $
@ -1653,7 +1661,7 @@ evalWorkflowRoleFor' tagActive mAuthId mwwId wRole route isWrite = $cachedHereBi
unless (uid `Set.member` uids) $ unless (uid `Set.member` uids) $
throwE =<< unauthorizedI MsgWorkflowRoleUserMismatch throwE =<< unauthorizedI MsgWorkflowRoleUserMismatch
return Authorized return Authorized
WorkflowRoleAuthorized{..} -> evalAuthTags tagActive workflowRoleAuthorized mAuthId route isWrite WorkflowRoleAuthorized{..} -> eval workflowRoleAuthorized mAuthId route isWrite
evalWorkflowRoleFor :: ( MonadHandler m evalWorkflowRoleFor :: ( MonadHandler m
, HandlerSite m ~ UniWorX , HandlerSite m ~ UniWorX
@ -1671,7 +1679,10 @@ evalWorkflowRoleFor mAuthId mwwId wRole route isWrite = do
tagActive <- if tagActive <- if
| isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags | isSelf -> fromMaybe def <$> lookupSessionJson SessionActiveAuthTags
| otherwise -> return . AuthTagActive $ const True | otherwise -> return . AuthTagActive $ const True
(result, deactivated) <- runWriterT $ evalWorkflowRoleFor' tagActive mAuthId mwwId wRole route isWrite (result, deactivated) <-
let eval :: forall m'. MonadAP m' => AuthTagsEval m'
eval dnf' mAuthId' route' isWrite' = evalAuthTags 'evalWorkflowRoleFor tagActive eval dnf' mAuthId' route' isWrite'
in runWriterT $ evalWorkflowRoleFor' eval mAuthId mwwId wRole route isWrite
when isSelf $ when isSelf $
tellSessionJson SessionInactiveAuthTags deactivated tellSessionJson SessionInactiveAuthTags deactivated
return result return result

View File

@ -37,9 +37,18 @@ import Foundation.DB
import Network.Wai.Parse (lbsBackEnd) import Network.Wai.Parse (lbsBackEnd)
import Control.Monad.Writer.Class (MonadWriter(..))
import UnliftIO.Pool (withResource) import UnliftIO.Pool (withResource)
import qualified Control.Monad.State.Class as State
import qualified Crypto.Hash as Crypto
import qualified Crypto.MAC.KMAC as Crypto
import qualified Data.Binary as Binary
import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E
data instance ButtonClass UniWorX data instance ButtonClass UniWorX
= BCIsButton = BCIsButton
@ -178,23 +187,102 @@ instance YesodAuthPersist UniWorX where
instance YesodMail UniWorX where instance YesodMail UniWorX where
defaultFromAddress = getsYesod $ view _appMailFrom defaultFromAddress = getsYesod $ view _appMailFrom
envelopeFromAddress = getsYesod $ view _appMailEnvelopeFrom
mailObjectIdDomain = getsYesod $ view _appMailObjectDomain mailObjectIdDomain = getsYesod $ view _appMailObjectDomain
mailVerp = getsYesod $ view _appMailVerp
mailDateTZ = return appTZ mailDateTZ = return appTZ
mailSmtp act = do mailSmtp act = do
pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool
withResource pool act withResource pool act
mailT ctx mail = defMailT ctx $ do mailT ctx mail = do
void setMailObjectIdRandom mailRecord <- newEmptyTMVarIO
setDateCurrent mailProcess <- allocateLinkedAsync $ do
replaceMailHeader "Sender" . Just =<< getsYesod (view $ _appMailFrom . _addressEmail) defMailT ctx $ do
void setMailObjectIdRandom
sentMailSentAt <- liftIO getCurrentTime
setDate sentMailSentAt
replaceMailHeader "Sender" . Just =<< getsYesod (view $ _appMailSender . to renderAddress)
(mRes, smtpData) <- listen mail (mRes, smtpData) <- listen mail
unless (view _MailSmtpDataSet smtpData)
setMailSmtpData
return mRes sentMailObjectId <- getMailObjectId
mContent <- State.get
smtpData' <- if | smtpData ^. _MailSmtpDataSet -> return smtpData
| otherwise -> getMailSmtpData
verpMode <- getsYesod $ view _appMailVerp
(smtpData'', sentMailBounceSecret) <- if
| Verp{..} <- verpMode
, [_] <- smtpData' ^.. _smtpRecipients . folded
, Just [l, d] <- previews (_smtpEnvelopeFrom . _Wrapped . _Just) (Text.splitOn "@") smtpData'
-> do
verpSecret <- getsYesod appVerpSecret
let bounceSecret = BounceSecret . Crypto.kmacGetDigest $ kmaclazy ("bounce" :: ByteString) verpSecret $ Binary.encode mContent
verpAddr = l <> Text.singleton verpSeparator <> verpPrefix <> "." <> toPathPiece bounceSecret <> "@" <> d
return ( smtpData' <> mempty { smtpEnvelopeFrom = Last $ Just verpAddr }
, Just bounceSecret
)
| otherwise -> return (smtpData', Nothing)
tell smtpData''
sentMailSentBy <- getsYesod appInstanceID
let sentMailRecipient = Nothing -- Fill in later
sentMailHeaders = MailHeaders $ mconcat
[ renderAddressHeader "From" [mailFrom mContent]
, fromMaybe [] $ do
toAddrs <- assertM' (not . null) $ mailTo mContent
return $ renderAddressHeader "To" toAddrs
, fromMaybe [] $ do
ccAddrs <- assertM' (not . null) $ mailCc mContent
return $ renderAddressHeader "Cc" ccAddrs
, fromMaybe [] $ do
bccAddrs <- assertM' (not . null) $ mailBcc mContent
return $ renderAddressHeader "Bcc" bccAddrs
, mailHeaders mContent
]
where
renderAddressHeader :: ByteString -> [Address] -> [(ByteString, Text)]
renderAddressHeader lbl = pure . (lbl, ) . Text.intercalate ", " . map renderAddress
sentMailContent = MailContent $ mailParts mContent
sentMailContentRef = SentMailContentKey . MailContentReference . Crypto.hashlazy $ Binary.encode sentMailContent
atomically $ putTMVar mailRecord
( smtpData'' ^.. _smtpRecipients . folded
, sentMailContent
, SentMail{..}
)
atomically . guardM $ isEmptyTMVar mailRecord
return mRes
(smtpRecipients, sentMailContentContent, sentMail) <- atomically $ takeTMVar mailRecord
void . tryAny . liftHandler . runDB . setSerializable $ do -- Ignore exceptions that occur during logging
sentMailRecipient <- if
| [Address _ (CI.mk -> recipAddr)] <- smtpRecipients -> do
recipUsers <- E.select . E.from $ \user -> do
E.where_ $ user E.^. UserDisplayEmail E.==. E.val recipAddr
E.||. user E.^. UserEmail E.==. E.val recipAddr
E.||. user E.^. UserIdent E.==. E.val recipAddr
return user
let recipUserCompare = mconcat
[ comparing $ Down . (== recipAddr) . userIdent . entityVal
, comparing $ Down . (== recipAddr) . userEmail . entityVal
, comparing $ Down . (== recipAddr) . userDisplayEmail . entityVal
]
return $ if
| ( bU : us ) <- sortBy recipUserCompare recipUsers
, maybe True (\u -> recipUserCompare bU u == LT) $ listToMaybe us
-> Just $ entityKey bU
| otherwise -> Nothing
| otherwise -> return Nothing
-- @insertUnique@ _does not_ work here
unlessM (exists [ SentMailContentHash ==. unSentMailContentKey (sentMailContentRef sentMail) ]) $
insert_ SentMailContent { sentMailContentHash = unSentMailContentKey $ sentMailContentRef sentMail
, sentMailContentContent
}
insert_ sentMail{ sentMailRecipient }
wait mailProcess -- Abort transaction if sending failed
wait mailProcess -- Rethrow exceptions for mailprocess; technically unnecessary due to linkage, doesn't hurt, though
instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where instance (MonadThrow m, MonadHandler m, HandlerSite m ~ UniWorX) => MonadCrypto m where
type MonadCryptoKey m = CryptoIDKey type MonadCryptoKey m = CryptoIDKey

View File

@ -43,6 +43,29 @@ deriving instance Generic GlobalWorkflowInstanceR
deriving instance Generic GlobalWorkflowWorkflowR deriving instance Generic GlobalWorkflowWorkflowR
deriving instance Generic (Route UniWorX) deriving instance Generic (Route UniWorX)
instance Ord (Route Auth) where
compare = compare `on` renderRoute
instance Ord (Route EmbeddedStatic) where
compare = compare `on` renderRoute
deriving instance Ord CourseR
deriving instance Ord SheetR
deriving instance Ord SubmissionR
deriving instance Ord MaterialR
deriving instance Ord TutorialR
deriving instance Ord ExamR
deriving instance Ord EExamR
deriving instance Ord CourseApplicationR
deriving instance Ord AllocationR
deriving instance Ord SchoolR
deriving instance Ord ExamOfficeR
deriving instance Ord CourseNewsR
deriving instance Ord CourseEventR
deriving instance Ord AdminWorkflowDefinitionR
deriving instance Ord GlobalWorkflowInstanceR
deriving instance Ord GlobalWorkflowWorkflowR
deriving instance Ord (Route UniWorX)
data RouteChildren data RouteChildren
type instance Children RouteChildren a = ChildrenRouteChildren a type instance Children RouteChildren a = ChildrenRouteChildren a
type family ChildrenRouteChildren a where type family ChildrenRouteChildren a where

View File

@ -57,6 +57,7 @@ data UniWorX = UniWorX
, appHealthReport :: TVar (Set (UTCTime, HealthReport)) , appHealthReport :: TVar (Set (UTCTime, HealthReport))
, appMemcached :: Maybe (AEAD.Key, Memcached.Connection) , appMemcached :: Maybe (AEAD.Key, Memcached.Connection)
, appUploadCache :: Maybe MinioConn , appUploadCache :: Maybe MinioConn
, appVerpSecret :: VerpSecret
} }
makeLenses_ ''UniWorX makeLenses_ ''UniWorX

View File

@ -40,7 +40,6 @@ import qualified Data.Binary as Binary
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import Control.Monad.Writer.Class (MonadWriter(..))
import Crypto.Hash.Conduit (sinkHash) import Crypto.Hash.Conduit (sinkHash)

View File

@ -18,9 +18,6 @@ import qualified Network.Wai as W
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Control.Monad.Reader.Class (MonadReader(..))
import Control.Monad.Writer.Class (MonadWriter(..))
import Yesod.Core.Types (GHState(..), HandlerData(..), RunHandlerEnv(rheSite, rheChild)) import Yesod.Core.Types (GHState(..), HandlerData(..), RunHandlerEnv(rheSite, rheChild))
import qualified Data.Map as Map import qualified Data.Map as Map

View File

@ -402,6 +402,7 @@ postCApplicationsR tid ssh csh = do
CourseApplicationsTableCsvSetRatingData{} -> CourseApplicationsTableCsvSetRating CourseApplicationsTableCsvSetRatingData{} -> CourseApplicationsTableCsvSetRating
CourseApplicationsTableCsvSetCommentData{} -> CourseApplicationsTableCsvSetComment CourseApplicationsTableCsvSetCommentData{} -> CourseApplicationsTableCsvSetComment
, dbtCsvCoarsenActionClass = const DBCsvActionExisting , dbtCsvCoarsenActionClass = const DBCsvActionExisting
, dbtCsvValidateActions = return ()
, dbtCsvExecuteActions = do , dbtCsvExecuteActions = do
C.mapM_ $ \case C.mapM_ $ \case
CourseApplicationsTableCsvSetVetoData{..} -> do CourseApplicationsTableCsvSetVetoData{..} -> do

View File

@ -305,7 +305,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
<*> aopt htmlField (fslI MsgCourseApplicationInstructions & setTooltip MsgCourseApplicationInstructionsTip) (cfAppInstructions <$> template) <*> aopt htmlField (fslI MsgCourseApplicationInstructions & setTooltip MsgCourseApplicationInstructionsTip) (cfAppInstructions <$> template)
<*> aopt (multiFileField' . fromMaybe (return ()) $ cfAppInstructionFiles =<< template) (fslI MsgCourseApplicationTemplate & setTooltip MsgCourseApplicationTemplateTip) (cfAppInstructionFiles <$> template) <*> aopt (multiFileField' . fromMaybe (return ()) $ cfAppInstructionFiles =<< template) (fslI MsgCourseApplicationTemplate & setTooltip MsgCourseApplicationTemplateTip) (cfAppInstructionFiles <$> template)
<*> apopt checkBoxField (fslI MsgCourseApplicationsText & setTooltip MsgCourseApplicationsTextTip) (cfAppText <$> template) <*> apopt checkBoxField (fslI MsgCourseApplicationsText & setTooltip MsgCourseApplicationsTextTip) (cfAppText <$> template)
<*> uploadModeForm (cfAppFiles <$> template) <*> uploadModeForm (fslI MsgCourseApplicationsFiles & setTooltip MsgCourseApplicationsFilesTip) (fmap cfAppFiles template <|> pure NoUpload)
<*> apopt checkBoxField (fslI MsgCourseApplicationRatingsVisible & setTooltip MsgCourseApplicationRatingsVisibleTip) (cfAppRatingsVisible <$> template) <*> apopt checkBoxField (fslI MsgCourseApplicationRatingsVisible & setTooltip MsgCourseApplicationRatingsVisibleTip) (cfAppRatingsVisible <$> template)
<*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity <*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template) & setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
@ -376,6 +376,10 @@ validateCourse = do
$ length (CI.original cfShort) <= 10 $ length (CI.original cfShort) <= 10
warnValidation MsgCourseNotAlwaysVisibleDuringRegistration warnValidation MsgCourseNotAlwaysVisibleDuringRegistration
$ NTop cfVisFrom <= NTop cfRegFrom && NTop cfRegTo <= NTop cfVisTo $ NTop cfVisFrom <= NTop cfRegFrom && NTop cfRegTo <= NTop cfVisTo
warnValidation MsgCourseApplicationInstructionsRecommended
$ (is _Just cfAppInstructions || is _Just cfAppInstructionFiles)
|| not (cfAppText || isn't _NoUpload cfAppFiles)
getCourseNewR :: Handler Html -- call via toTextUrl getCourseNewR :: Handler Html -- call via toTextUrl

View File

@ -1,7 +1,7 @@
module Handler.Course.Register module Handler.Course.Register
( ButtonCourseRegister(..) ( ButtonCourseRegister(..)
, CourseRegisterForm(..) , CourseRegisterForm(..)
, courseRegisterForm , courseRegisterForm, courseMayReRegister
, getCRegisterR, postCRegisterR , getCRegisterR, postCRegisterR
, deregisterParticipant , deregisterParticipant
) where ) where
@ -147,18 +147,29 @@ courseRegisterForm (Entity cid Course{..}) = liftHandler $ do
E.||. isCourseExamCorrector muid ata (course E.^. CourseId) E.||. isCourseExamCorrector muid ata (course E.^. CourseId)
) )
mayReRegister <- liftHandler . runDB . courseMayReRegister $ Entity cid Course{..}
when (is _Just $ registration >>= courseParticipantAllocated . entityVal) $ when (is _Just $ registration >>= courseParticipantAllocated . entityVal) $
wformMessage =<< messageIconI Warning IconExamRegisterFalse MsgCourseDeregistrationAllocationLog wformMessage =<< messageIconI Warning IconExamRegisterFalse MsgCourseDeregistrationAllocationLog
when (is _Just (registration >>= courseParticipantAllocated . entityVal) && courseDeregisterNoShow) $ when (is _Just (registration >>= courseParticipantAllocated . entityVal) && courseDeregisterNoShow) $
wformMessage =<< messageIconI Warning IconEnrolFalse MsgCourseDeregistrationNoShow wformMessage =<< messageIconI Warning IconEnrolFalse MsgCourseDeregistrationNoShow
when (isRegistered && not mayViewCourseAfterDeregistration) $ when (isRegistered && not mayViewCourseAfterDeregistration) $
wformMessage =<< messageIconI Warning IconEnrolFalse MsgCourseDeregistrationFromInvisibleCourse wformMessage =<< messageIconI Warning IconEnrolFalse MsgCourseDeregistrationFromInvisibleCourse
unless mayReRegister $
wformMessage =<< messageIconI Warning IconEnrolFalse MsgCourseDeregistrationNoReRegistration
return $ CourseRegisterForm return $ CourseRegisterForm
<$ secretRes <$ secretRes
<*> appTextRes <*> appTextRes
<*> appFilesRes <*> appFilesRes
courseMayReRegister :: Entity Course -> DB Bool
courseMayReRegister (Entity cid Course{..}) = do
registrations <- count [ CourseParticipantState ==. CourseParticipantActive, CourseParticipantCourse ==. cid ]
let capacity = maybe True (>= registrations) courseCapacity
wouldHaveWriteAccessTo [(AuthCapacity, capacity), (AuthCourseRegistered, False)] $ CourseR courseTerm courseSchool courseShorthand CRegisterR
-- | Workaround for klicking register button without being logged in. -- | Workaround for klicking register button without being logged in.
-- After log in, the user sees a "get request not supported" error. -- After log in, the user sees a "get request not supported" error.
@ -273,7 +284,6 @@ deleteApplicationFiles appId = deleteWhere [ CourseApplicationFileApplication ==
deregisterParticipant :: UserId -> CourseId -> DB () deregisterParticipant :: UserId -> CourseId -> DB ()
deregisterParticipant uid cid = do deregisterParticipant uid cid = do
deleteApplications uid cid
part <- fmap (assertM . has $ _entityVal . _courseParticipantState . _CourseParticipantActive) . getBy $ UniqueParticipant uid cid part <- fmap (assertM . has $ _entityVal . _courseParticipantState . _CourseParticipantActive) . getBy $ UniqueParticipant uid cid
forM_ part $ \(Entity partId CourseParticipant{}) -> do forM_ part $ \(Entity partId CourseParticipant{}) -> do
update partId [CourseParticipantState =. CourseParticipantInactive False] update partId [CourseParticipantState =. CourseParticipantInactive False]

View File

@ -28,7 +28,7 @@ getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do getCShowR tid ssh csh = do
mbAid <- maybeAuthId mbAid <- maybeAuthId
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
(cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen) <- runDB . maybeT notFound $ do (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister) <- runDB . maybeT notFound $ do
[(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration, E.Value hasAllocationRegistrationOpen)] [(E.Entity cid course, E.Value courseVisible, E.Value schoolName, E.Value participants, fmap entityVal -> registration, E.Value hasAllocationRegistrationOpen)]
<- lift . E.select . E.from $ <- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do \((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
@ -106,7 +106,9 @@ getCShowR tid ssh csh = do
return $ submissionGroup E.^. SubmissionGroupName return $ submissionGroup E.^. SubmissionGroupName
let submissionGroup = guardOnM (hasSubmissionGroups && is _Just registration) submissionGroup' let submissionGroup = guardOnM (hasSubmissionGroups && is _Just registration) submissionGroup'
return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen) mayReRegister <- lift . courseMayReRegister $ Entity cid course
return (cid,course,courseVisible,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication,news,events,submissionGroup,hasAllocationRegistrationOpen,mayReRegister)
let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course let mDereg' = maybe id min (allocationOverrideDeregister =<< mAllocation) <$> courseDeregisterUntil course
mDereg <- traverse (formatTime SelFormatDateTime) mDereg' mDereg <- traverse (formatTime SelFormatDateTime) mDereg'

View File

@ -43,6 +43,8 @@ import Control.Lens.Indexed ((<.), (.>))
import Jobs.Queue import Jobs.Queue
import qualified Control.Monad.State.Class as State
type ExamUserTableExpr = ( E.SqlExpr (Entity ExamRegistration) type ExamUserTableExpr = ( E.SqlExpr (Entity ExamRegistration)
`E.InnerJoin` E.SqlExpr (Entity User) `E.InnerJoin` E.SqlExpr (Entity User)
@ -613,8 +615,7 @@ postEUsersR tid ssh csh examn = do
-> 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
unless isPart $ yieldM $ bool ExamUserCsvCourseRegisterData ExamUserCsvRegisterData isPart uid <$> lookupOccurrence dbCsvNew
yieldM $ ExamUserCsvCourseRegisterData uid <$> lookupOccurrence dbCsvNew
iforMOf_ (ifolded <. _Just) (csvEUserExamPartResults dbCsvNew) $ \epNumber epRes -> iforMOf_ (ifolded <. _Just) (csvEUserExamPartResults dbCsvNew) $ \epNumber epRes ->
when (epNumber `elem` examPartNumbers) $ when (epNumber `elem` examPartNumbers) $
@ -706,6 +707,22 @@ postEUsersR tid ssh csh examn = do
ExamUserCsvRegister -> DBCsvActionNew ExamUserCsvRegister -> DBCsvActionNew
ExamUserCsvDeregister -> DBCsvActionMissing ExamUserCsvDeregister -> DBCsvActionMissing
_other -> DBCsvActionExisting _other -> DBCsvActionExisting
, dbtCsvValidateActions = do
selectedActions <- State.get
availableActions <- ask
let missingExamDataUsers = flip filter examDataUsers $ \uid -> any (isRegisterAction uid) availableActions && none (isRegisterAction uid) selectedActions
where
examDataUsers = flip mapMaybe selectedActions $ \case
ExamUserCsvSetResultData{..} -> Just examUserCsvActUser
ExamUserCsvSetBonusData{..} -> Just examUserCsvActUser
ExamUserCsvSetPartResultData{..} -> Just examUserCsvActUser
_other -> Nothing
isRegisterAction uid = \case
ExamUserCsvCourseRegisterData{..} -> uid == examUserCsvActUser
ExamUserCsvRegisterData{..} -> uid == examUserCsvActUser
_other -> False
unless (null missingExamDataUsers) $
tellMPoint $ messageI Error MsgExamUsersExamDataRequiresRegistration
, dbtCsvExecuteActions = do , dbtCsvExecuteActions = do
C.mapM_ $ \case C.mapM_ $ \case
ExamUserCsvCourseRegisterData{..} -> do ExamUserCsvCourseRegisterData{..} -> do

View File

@ -476,8 +476,6 @@ makeProfileData (Entity uid User{..}) = do
examTable = i18n MsgPersonalInfoExamAchievementsWip examTable = i18n MsgPersonalInfoExamAchievementsWip
ownTutorialTable = i18n MsgPersonalInfoOwnTutorialsWip ownTutorialTable = i18n MsgPersonalInfoOwnTutorialsWip
tutorialTable = i18n MsgPersonalInfoTutorialsWip tutorialTable = i18n MsgPersonalInfoTutorialsWip
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication
lastLdapSync <- traverse (formatTime SelFormatDateTime) userLastLdapSynchronisation
cID <- encrypt uid cID <- encrypt uid
mCRoute <- getCurrentRoute mCRoute <- getCurrentRoute

View File

@ -52,18 +52,16 @@ hijackUserForm csrf = do
data UserAction = UserLdapSync | UserHijack data UserAction = UserLdapSync | UserHijack
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance Universe UserAction
instance Finite UserAction
nullaryPathPiece ''UserAction $ camelToPathPiece' 1 nullaryPathPiece ''UserAction $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''UserAction id embedRenderMessage ''UniWorX ''UserAction id
data AllUsersAction = AllUsersLdapSync data AllUsersAction = AllUsersLdapSync
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance Universe AllUsersAction
instance Finite AllUsersAction
nullaryPathPiece ''AllUsersAction $ camelToPathPiece' 1 nullaryPathPiece ''AllUsersAction $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''AllUsersAction id embedRenderMessage ''UniWorX ''AllUsersAction id
@ -276,6 +274,19 @@ instance Button UniWorX ButtonAuthMode where
btnClasses _ = [BCIsButton] btnClasses _ = [BCIsButton]
data UserAssimilateButton = BtnUserAssimilate
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance Button UniWorX UserAssimilateButton where
btnClasses _ = [BCIsButton, BCPrimary]
nullaryPathPiece ''UserAssimilateButton $ camelToPathPiece' 2
embedRenderMessage ''UniWorX ''UserAssimilateButton id
getAdminUserR, postAdminUserR :: CryptoUUIDUser -> Handler Html getAdminUserR, postAdminUserR :: CryptoUUIDUser -> Handler Html
getAdminUserR = postAdminUserR getAdminUserR = postAdminUserR
postAdminUserR uuid = do postAdminUserR uuid = do
@ -392,9 +403,33 @@ postAdminUserR uuid = do
| otherwise | otherwise
-> addMessageI Info MsgUserSystemFunctionsNotChanged -> addMessageI Info MsgUserSystemFunctionsNotChanged
redirect $ AdminUserR uuid redirect $ AdminUserR uuid
((rightsResult, rightsFormWidget),rightsFormEnctype) <- runFormPost userRightsForm let assimilateForm' = renderAForm FormStandard $
((authResult, authFormWidget),authFormEnctype) <- runFormPost userAuthenticationForm areq (checkMap (first $ const MsgAssimilateUserNotFound) Right $ userField False Nothing) (fslI MsgUserAssimilateUser) Nothing
((systemFunctionsResult, systemFunctionsWidget),systemFunctionsEnctype) <- runFormPost . identifyForm FIDUserSystemFunctions $ renderAForm FormStandard systemFunctionsForm' assimilateAction oldUserId = do
res <- try . runDB . setSerializable $ assimilateUser uid oldUserId
case res of
Left (err :: UserAssimilateException) ->
addMessageModal Error (i18n MsgAssimilateUserHaveError) $ Right
[whamlet|
<div .shown>
#{tshow err}
|]
Right warnings -> do
unless (null warnings) $
addMessageModal Warning (i18n MsgAssimilateUserHaveWarnings) $ Right
[whamlet|
$newline never
<ul>
$forall warning <- warnings
<li .shown>
#{tshow warning}
|]
addMessageI Success MsgAssimilateUserSuccess
redirect $ AdminUserR uuid
((rightsResult, rightsFormWidget), rightsFormEnctype) <- runFormPost . identifyForm FIDUserRights $ userRightsForm
((authResult, authFormWidget), authFormEnctype) <- runFormPost . identifyForm FIDUserAuthentication $ userAuthenticationForm
((systemFunctionsResult, systemFunctionsWidget), systemFunctionsEnctype) <- runFormPost . identifyForm FIDUserSystemFunctions $ renderAForm FormStandard systemFunctionsForm'
((assimilateFormResult, assimilateFormWidget), assimilateFormEnctype) <- runFormPost $ identifyForm FIDUserAssimilate assimilateForm'
let rightsForm = wrapForm rightsFormWidget def let rightsForm = wrapForm rightsFormWidget def
{ formAction = Just . SomeRoute $ AdminUserR uuid { formAction = Just . SomeRoute $ AdminUserR uuid
, formEncoding = rightsFormEnctype , formEncoding = rightsFormEnctype
@ -408,9 +443,14 @@ postAdminUserR uuid = do
{ formAction = Just . SomeRoute $ AdminUserR uuid { formAction = Just . SomeRoute $ AdminUserR uuid
, formEncoding = systemFunctionsEnctype , formEncoding = systemFunctionsEnctype
} }
assimilateForm = wrapForm' BtnUserAssimilate assimilateFormWidget def
{ formAction = Just . SomeRoute $ AdminUserR uuid
, formEncoding = assimilateFormEnctype
}
formResult rightsResult userRightsAction formResult rightsResult userRightsAction
formResult authResult userAuthenticationAction formResult authResult userAuthenticationAction
formResult systemFunctionsResult userSystemFunctionsAction formResult systemFunctionsResult userSystemFunctionsAction
formResult assimilateFormResult assimilateAction
let heading = let heading =
[whamlet|_{MsgAdminUserHeadingFor} ^{nameEmailWidget userEmail userDisplayName userSurname}|] [whamlet|_{MsgAdminUserHeadingFor} ^{nameEmailWidget userEmail userDisplayName userSurname}|]
-- Delete Button needed in data-delete -- Delete Button needed in data-delete
@ -422,7 +462,7 @@ postAdminUserR uuid = do
} }
userDataWidget <- runDB $ makeProfileData $ Entity uid user userDataWidget <- runDB $ makeProfileData $ Entity uid user
siteLayout heading $ do siteLayout heading $ do
let deleteWidget = $(i18nWidgetFile "data-delete") let _deleteWidget = $(i18nWidgetFile "data-delete")
$(widgetFile "adminUser") $(widgetFile "adminUser")

View File

@ -446,6 +446,7 @@ makeExternalExamUsersTable mode (Entity eeId ExternalExam{..}) = do
when (csvEUserExamResult /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultResult) $ when (csvEUserExamResult /= dbCsvOld ^. resultResult . _entityVal . _externalExamResultResult) $
yield $ ExternalExamUserCsvSetResultData (E.unValue dbCsvOldKey) csvEUserExamResult yield $ ExternalExamUserCsvSetResultData (E.unValue dbCsvOldKey) csvEUserExamResult
, dbtCsvValidateActions = return ()
, dbtCsvClassifyAction = \case , dbtCsvClassifyAction = \case
ExternalExamUserCsvRegisterData{} -> ExternalExamUserCsvRegister ExternalExamUserCsvRegisterData{} -> ExternalExamUserCsvRegister
ExternalExamUserCsvSetTimeData{} -> ExternalExamUserCsvSetTime ExternalExamUserCsvSetTimeData{} -> ExternalExamUserCsvSetTime

View File

@ -48,7 +48,6 @@ import qualified Data.Vector as Vector
import qualified Data.HashMap.Lazy as HashMap import qualified Data.HashMap.Lazy as HashMap
import Control.Monad.Writer.Class
import Control.Monad.Error.Class (MonadError(..)) import Control.Monad.Error.Class (MonadError(..))
import Data.Aeson (eitherDecodeStrict') import Data.Aeson (eitherDecodeStrict')
@ -586,8 +585,8 @@ studyFeaturesFieldFor mRestr isOptional oldFeatures mbuid = selectField $ do
} }
uploadModeForm :: Maybe UploadMode -> AForm Handler UploadMode uploadModeForm :: FieldSettings UniWorX -> Maybe UploadMode -> AForm Handler UploadMode
uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUploadMode <$> prev) uploadModeForm fs prev = multiActionA actions fs (classifyUploadMode <$> prev)
where where
actions :: Map UploadModeDescr (AForm Handler UploadMode) actions :: Map UploadModeDescr (AForm Handler UploadMode)
actions = Map.fromList actions = Map.fromList
@ -683,10 +682,10 @@ submissionModeForm prev = explainedMultiActionA actions opts (fslI MsgSheetSubmi
, pure $ SubmissionMode True Nothing , pure $ SubmissionMode True Nothing
) )
, ( SubmissionModeUser , ( SubmissionModeUser
, SubmissionMode False . Just <$> uploadModeForm (prev ^? _Just . _submissionModeUser . _Just) , SubmissionMode False . Just <$> uploadModeForm (fslI MsgSheetUploadMode) (prev ^? _Just . _submissionModeUser . _Just)
) )
, ( SubmissionModeBoth , ( SubmissionModeBoth
, SubmissionMode True . Just <$> uploadModeForm (prev ^? _Just . _submissionModeUser . _Just) , SubmissionMode True . Just <$> uploadModeForm (fslI MsgSheetUploadMode) (prev ^? _Just . _submissionModeUser . _Just)
) )
] ]

View File

@ -31,8 +31,6 @@ import Data.Map ((!))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Foldable as Fold import qualified Data.Foldable as Fold
import Control.Monad.Reader.Class (MonadReader(local))
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Algebra.Lattice.Ordered (Ordered(..)) import Algebra.Lattice.Ordered (Ordered(..))

View File

@ -2,8 +2,6 @@ module Handler.Utils.Table.Cells where
import Import hiding (link) import Import hiding (link)
import Control.Monad.Writer.Class (MonadWriter(..))
import Text.Blaze (ToMarkup(..)) import Text.Blaze (ToMarkup(..))
import Handler.Utils.Table.Pagination import Handler.Utils.Table.Pagination

View File

@ -69,7 +69,7 @@ import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import Control.Monad.RWS (RWST(..), execRWS) import Control.Monad.RWS (RWST(..), execRWS, execRWST)
import Control.Monad.State (evalStateT, execStateT) import Control.Monad.State (evalStateT, execStateT)
import Control.Monad.Trans.Maybe import Control.Monad.Trans.Maybe
import Control.Monad.State.Class (modify) import Control.Monad.State.Class (modify)
@ -420,6 +420,9 @@ data DBCsvException k'
{ dbCsvExceptionRow :: NamedRecord { dbCsvExceptionRow :: NamedRecord
, dbCsvException :: Text , dbCsvException :: Text
} }
| DBCsvUnavailableActionRequested
{ dbCsvActions :: Set Value
}
deriving (Show, Typeable) deriving (Show, Typeable)
makeLenses_ ''DBCsvException makeLenses_ ''DBCsvException
@ -598,6 +601,7 @@ data DBTCsvDecode r' k' csv = forall route csvAction csvActionClass csvException
) => DBTCsvDecode ) => DBTCsvDecode
{ dbtCsvRowKey :: csv -> MaybeT DB k' { dbtCsvRowKey :: csv -> MaybeT DB k'
, dbtCsvComputeActions :: DBCsvDiff r' csv k' -> ConduitT () csvAction DB () , dbtCsvComputeActions :: DBCsvDiff r' csv k' -> ConduitT () csvAction DB ()
, dbtCsvValidateActions :: RWST (Set csvAction) [Message] [csvAction] DB ()
, dbtCsvClassifyAction :: csvAction -> csvActionClass , dbtCsvClassifyAction :: csvAction -> csvActionClass
, dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode , dbtCsvCoarsenActionClass :: csvActionClass -> DBCsvActionMode
, dbtCsvExecuteActions :: ConduitT csvAction Void (YesodJobDB UniWorX) route , dbtCsvExecuteActions :: ConduitT csvAction Void (YesodJobDB UniWorX) route
@ -1177,6 +1181,8 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
<input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked> <input id=#{theId} *{attrs} type=checkbox name=#{name} value=#{either id id val} :defaultChecked (dbtCsvClassifyAction act):checked>
|] |]
fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False fieldView sJsonField (actionIdent act) (toPathPiece PostDBCsvImportAction) vAttrs (Right act) False
availableActs :: Widget
availableActs = fieldView (secretJsonField :: Field Handler (Set csvAction)) "" (toPathPiece PostDBCsvImportAvailableActions) [] (Right . Set.unions $ Map.elems actionMap) False
(csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandler . generateFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation")) (csvImportConfirmForm', csvImportConfirmEnctype) <- liftHandler . generateFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \csrf -> return (error "No meaningful FormResult", $(widgetFile "csv-import-confirmation"))
let csvImportConfirmForm = wrapForm csvImportConfirmForm' FormSettings let csvImportConfirmForm = wrapForm csvImportConfirmForm' FormSettings
{ formMethod = POST { formMethod = POST
@ -1231,6 +1237,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
<section> <section>
^{csvReImport} ^{csvReImport}
|] |]
other -> throwM other
, Catch.Handler $ \(csvParseError :: CsvParseError) , Catch.Handler $ \(csvParseError :: CsvParseError)
-> liftHandler $ sendResponseStatus badRequest400 =<< do -> liftHandler $ sendResponseStatus badRequest400 =<< do
mr <- getMessageRender mr <- getMessageRender
@ -1389,18 +1396,29 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
] ]
((csvImportConfirmRes, _confirmView), _enctype) <- case dbtCsvDecode of ((csvImportConfirmRes, _confirmView), _enctype) <- case dbtCsvDecode of
Just (DBTCsvDecode{dbtCsvExecuteActions} :: DBTCsvDecode r' k' csv) -> do Just (DBTCsvDecode{dbtCsvExecuteActions, dbtCsvValidateActions} :: DBTCsvDecode r' k' csv) -> do
lift . runFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \_csrf -> do lift . runFormPost . withButtonForm' [BtnCsvImportConfirm, BtnCsvImportAbort] . identifyForm (FIDDBTableCsvImportConfirm dbtIdent) $ \_csrf -> do
availableActs <- fromMaybe Set.empty <$> globalPostParamField PostDBCsvImportAvailableActions secretJsonField
acts <- globalPostParamFields PostDBCsvImportAction secretJsonField acts <- globalPostParamFields PostDBCsvImportAction secretJsonField
return . (, mempty) $ if return . (, mempty) . FormSuccess $ if
| null acts -> FormSuccess $ do | unavailableActs <- filter (`Set.notMember` availableActs) acts
addMessageI Info MsgCsvImportAborted , not $ null unavailableActs -> do
redirect $ tblLink id throwM . DBCsvUnavailableActionRequested @k' . Set.fromList $ map toJSON unavailableActs
| otherwise -> FormSuccess $ do | otherwise -> do
finalDest <- runDBJobs' . runConduit $ C.sourceList acts .| dbtCsvExecuteActions (acts', validationMsgs) <- execRWST dbtCsvValidateActions availableActs acts
addMessageI Success . MsgCsvImportSuccessful $ length acts if | not $ null validationMsgs -> do
E.transactionSave mapM_ addMessage' validationMsgs
redirect finalDest E.transactionUndo
redirect $ tblLink id
| null acts' -> do
addMessageI Info MsgCsvImportAborted
redirect $ tblLink id
| otherwise -> do
finalDest <- runDBJobs' . runConduit $ C.sourceList acts' .| dbtCsvExecuteActions
addMessageI Success . MsgCsvImportSuccessful $ length acts'
E.transactionSave
redirect finalDest
_other -> return ((FormMissing, mempty), mempty) _other -> return ((FormMissing, mempty), mempty)
formResult csvImportConfirmRes $ \case formResult csvImportConfirmRes $ \case
(_, BtnCsvImportAbort) -> do (_, BtnCsvImportAbort) -> do

View File

@ -6,6 +6,8 @@ module Handler.Utils.Users
, matchesName , matchesName
, GuessUserInfo(..) , GuessUserInfo(..)
, guessUser , guessUser
, UserAssimilateException(..), UserAssimilateExceptionReason(..)
, assimilateUser
) where ) where
import Import import Import
@ -19,17 +21,23 @@ import Data.Maybe (fromJust)
import qualified Data.List.NonEmpty as NonEmpty (fromList) import qualified Data.List.NonEmpty as NonEmpty (fromList)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.PostgreSQL as E
import qualified Database.Esqueleto.Utils as E import qualified Database.Esqueleto.Utils as E
import qualified Data.Conduit.Combinators as C
import qualified Data.MultiSet as MultiSet import qualified Data.MultiSet as MultiSet
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Text as Text import qualified Data.Text as Text
import Jobs.Types(Job, JobChildren)
computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256 computeUserAuthenticationDigest :: AuthenticationMode -> Digest SHA3_256
computeUserAuthenticationDigest = hashlazy . JSON.encode computeUserAuthenticationDigest = hashlazy . JSON.encode
@ -172,3 +180,604 @@ guessUser (((Set.toList . toNullable) <$>) . Set.toList . dnfTerms -> criteria)
-> mapM doLdap userMatrs >>= maybe (go True) (return . Just) . convertLdapResults . catMaybes -> mapM doLdap userMatrs >>= maybe (go True) (return . Just) . convertLdapResults . catMaybes
| otherwise | otherwise
-> return Nothing -> return Nothing
data UserAssimilateException = UserAssimilateException
{ userAssimilateOldUser, userAssimilateNewUser :: UserId
, userAssimilateException :: UserAssimilateExceptionReason
} deriving (Eq, Ord, Show, Generic, Typeable)
deriving anyclass (Exception)
data UserAssimilateExceptionReason
= UserAssimilateExternalExamResultDifferentResult (Entity ExternalExamResult) (Entity ExternalExamResult)
| UserAssimilateCourseParticipantDifferentAllocation (Entity CourseParticipant) (Entity CourseParticipant)
| UserAssimilateSubmissionGroupUserMultiple (Entity SubmissionGroupUser) (Entity SubmissionGroupUser)
| UserAssimilateAllocationUserDifferentPriority (Entity AllocationUser) (Entity AllocationUser)
| UserAssimilateAllocationDeregisterDuplicateCourse (Entity AllocationDeregister) (Entity AllocationDeregister)
| UserAssimilateExamRegistrationDifferentOccurrence (Entity ExamRegistration) (Entity ExamRegistration)
| UserAssimilateExamPartResultDifferentResult (Entity ExamPartResult) (Entity ExamPartResult)
| UserAssimilateExamBonusDifferentBonus (Entity ExamBonus) (Entity ExamBonus)
| UserAssimilateExamResultDifferentResult (Entity ExamResult) (Entity ExamResult)
| UserAssimilatePersonalisedSheetFileDifferentContent (Entity PersonalisedSheetFile) (Entity PersonalisedSheetFile)
| UserAssimilateTutorialParticipantCollidingRegGroups (Entity TutorialParticipant) (Entity TutorialParticipant)
deriving (Eq, Ord, Show, Generic, Typeable)
assimilateUser :: UserId -- ^ @newUserId@
-> UserId -- ^ @oldUserId@
-> DB (Set UserAssimilateException) -- ^ Warnings
-- ^ Move all relevant properties (submissions, corrections, grades, ...) from @oldUserId@ to @newUserId@
--
-- Fatal errors are thrown, non-fatal warnings are returned
assimilateUser newUserId oldUserId = mapReaderT execWriterT $ do
E.insertSelectWithConflict
UniqueCourseFavourite
(E.from $ \courseFavourite -> do
E.where_ $ courseFavourite E.^. CourseFavouriteUser E.==. E.val oldUserId
return $ CourseFavourite
E.<# E.val newUserId
E.<&> (courseFavourite E.^. CourseFavouriteCourse)
E.<&> (courseFavourite E.^. CourseFavouriteReason)
E.<&> (courseFavourite E.^. CourseFavouriteLastVisit)
)
(\current excluded -> [ CourseFavouriteLastVisit E.=. E.max (current E.^. CourseFavouriteLastVisit) (excluded E.^. CourseFavouriteLastVisit) ])
deleteWhere [ CourseFavouriteUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueCourseNoFavourite
(E.from $ \courseNoFavourite -> do
E.where_ $ courseNoFavourite E.^. CourseNoFavouriteUser E.==. E.val oldUserId
return $ CourseNoFavourite
E.<# E.val newUserId
E.<&> (courseNoFavourite E.^. CourseNoFavouriteCourse)
)
(\_current _excluded -> [])
deleteWhere [ CourseNoFavouriteUser ==. oldUserId ]
let getCourseApplications = selectSource [ CourseApplicationUser ==. oldUserId ] []
upsertCourseApplication (Entity oldAppId oldApp) = do
newApp <- selectList [CourseApplicationUser ==. newUserId, CourseApplicationCourse ==. courseApplicationCourse oldApp, CourseApplicationAllocation ==. courseApplicationAllocation oldApp] [LimitTo 1]
case newApp of
(_ : _) -> return ()
[] -> do
newAppId <- insert oldApp
{ courseApplicationUser = newUserId
}
updateWhere [ CourseApplicationFileApplication ==. oldAppId ] [ CourseApplicationFileApplication =. newAppId ]
delete oldAppId
in runConduit $ getCourseApplications .| C.mapM_ upsertCourseApplication
E.insertSelectWithConflict
UniqueExamOfficeField
(E.from $ \examOfficeField -> do
E.where_ $ examOfficeField E.^. ExamOfficeFieldOffice E.==. E.val oldUserId
return $ ExamOfficeField
E.<# E.val newUserId
E.<&> (examOfficeField E.^. ExamOfficeFieldField)
E.<&> (examOfficeField E.^. ExamOfficeFieldForced)
)
(\current excluded -> [ ExamOfficeFieldForced E.=. (current E.^. ExamOfficeFieldForced E.||. excluded E.^. ExamOfficeFieldForced) ])
deleteWhere [ ExamOfficeFieldOffice ==. oldUserId ]
E.insertSelectWithConflict
UniqueExamOfficeUser
(E.from $ \examOfficeUser -> do
E.where_ $ examOfficeUser E.^. ExamOfficeUserOffice E.==. E.val oldUserId
return $ ExamOfficeUser
E.<# E.val newUserId
E.<&> (examOfficeUser E.^. ExamOfficeUserUser)
)
(\_current _excluded -> [])
deleteWhere [ ExamOfficeUserOffice ==. oldUserId ]
E.insertSelectWithConflict
UniqueExamOfficeUser
(E.from $ \examOfficeUser -> do
E.where_ $ examOfficeUser E.^. ExamOfficeUserUser E.==. E.val oldUserId
return $ ExamOfficeUser
E.<# (examOfficeUser E.^. ExamOfficeUserOffice)
E.<&> E.val newUserId
)
(\_current _excluded -> [])
deleteWhere [ ExamOfficeUserUser ==. oldUserId ]
E.insertSelect . E.from $ \examOfficeResultSynced -> do
E.where_ $ examOfficeResultSynced E.^. ExamOfficeResultSyncedOffice E.==. E.val oldUserId
return $ ExamOfficeResultSynced
E.<# (examOfficeResultSynced E.^. ExamOfficeResultSyncedSchool)
E.<&> E.val newUserId
E.<&> (examOfficeResultSynced E.^. ExamOfficeResultSyncedResult)
E.<&> (examOfficeResultSynced E.^. ExamOfficeResultSyncedTime)
deleteWhere [ ExamOfficeResultSyncedOffice ==. oldUserId ]
E.insertSelect . E.from $ \examOfficeExternalResultSynced -> do
E.where_ $ examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedOffice E.==. E.val oldUserId
return $ ExamOfficeExternalResultSynced
E.<# (examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedSchool)
E.<&> E.val newUserId
E.<&> (examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedResult)
E.<&> (examOfficeExternalResultSynced E.^. ExamOfficeExternalResultSyncedTime)
deleteWhere [ ExamOfficeExternalResultSyncedOffice ==. oldUserId ]
let getExternalExamResults = selectSource [ ExternalExamResultUser ==. oldUserId ] []
upsertExternalExamResult oldEEREnt@(Entity oldEERId oldEER) = do
newEER' <- getBy $ UniqueExternalExamResult (externalExamResultExam oldEER) newUserId
newEERId <- case newEER' of
Just newEEREnt@(Entity _ newEER)
| ((/=) `on` externalExamResultResult) newEER oldEER
|| ((/=) `on` externalExamResultTime) newEER oldEER
-> tellError $ UserAssimilateExternalExamResultDifferentResult oldEEREnt newEEREnt
Just (Entity newEERId newEER) -> newEERId <$ update newEERId
[ ExternalExamResultLastChanged =. (max `on` externalExamResultLastChanged) oldEER newEER
]
Nothing -> insert oldEER
{ externalExamResultUser = newUserId
}
updateWhere [ ExamOfficeExternalResultSyncedResult ==. oldEERId ] [ ExamOfficeExternalResultSyncedResult =. newEERId ]
delete oldEERId
in runConduit $ getExternalExamResults .| C.mapM_ upsertExternalExamResult
E.insertSelectWithConflict
UniqueExternalExamStaff
(E.from $ \externalExamStaff -> do
E.where_ $ externalExamStaff E.^. ExternalExamStaffUser E.==. E.val oldUserId
return $ ExternalExamStaff
E.<# E.val newUserId
E.<&> (externalExamStaff E.^. ExternalExamStaffExam)
)
(\_current _excluded -> [])
deleteWhere [ ExternalExamStaffUser ==. oldUserId ]
updateWhere [ SubmissionRatingBy ==. Just oldUserId ] [ SubmissionRatingBy =. Just newUserId ]
updateWhere [ SubmissionEditUser ==. Just oldUserId ] [ SubmissionEditUser =. Just newUserId ]
E.insertSelectWithConflict
UniqueSubmissionUser
(E.from $ \submissionUser -> do
E.where_ $ submissionUser E.^. SubmissionUserUser E.==. E.val oldUserId
return $ SubmissionUser
E.<# E.val newUserId
E.<&> (submissionUser E.^. SubmissionUserSubmission)
)
(\_current _excluded -> [])
deleteWhere [ SubmissionUserUser ==. oldUserId ]
do
collisions <- E.select . E.from $ \((submissionGroupUserA `E.InnerJoin` submissionGroupA) `E.InnerJoin` (submissionGroupUserB `E.InnerJoin` submissionGroupB)) -> do
E.on $ submissionGroupB E.^. SubmissionGroupId E.==. submissionGroupUserB E.^. SubmissionGroupUserSubmissionGroup
E.on $ submissionGroupUserA E.^. SubmissionGroupUserSubmissionGroup E.!=. submissionGroupUserB E.^. SubmissionGroupUserSubmissionGroup
E.&&. submissionGroupUserA E.^. SubmissionGroupUserUser E.==. E.val oldUserId
E.&&. submissionGroupUserB E.^. SubmissionGroupUserUser E.==. E.val newUserId
E.on $ submissionGroupA E.^. SubmissionGroupId E.==. submissionGroupUserA E.^. SubmissionGroupUserSubmissionGroup
E.where_ $ submissionGroupA E.^. SubmissionGroupCourse E.==. submissionGroupB E.^. SubmissionGroupCourse
return (submissionGroupUserA, submissionGroupUserB)
forM_ collisions $ \(submissionGroupUserA, submissionGroupUserB) ->
tellWarning $ UserAssimilateSubmissionGroupUserMultiple submissionGroupUserA submissionGroupUserB
E.insertSelectWithConflict
UniqueSubmissionGroupUser
(E.from $ \submissionGroupUser -> do
E.where_ $ submissionGroupUser E.^. SubmissionGroupUserUser E.==. E.val oldUserId
return $ SubmissionGroupUser
E.<# (submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup)
E.<&> E.val newUserId
)
(\_current _excluded -> [])
deleteWhere [ SubmissionGroupUserUser ==. oldUserId ]
updateWhere [ TransactionLogInitiator ==. Just oldUserId ] [ TransactionLogInitiator =. Just newUserId ]
-- We're not updating info; doing that would probably be too slow
-- Just check for `TransactionUserAssimilated` entries and correct manually
updateWhere [ CourseEditUser ==. oldUserId ] [ CourseEditUser =. newUserId ]
E.insertSelectWithConflict
UniqueLecturer
(E.from $ \lecturer -> do
E.where_ $ lecturer E.^. LecturerUser E.==. E.val oldUserId
return $ Lecturer
E.<# E.val newUserId
E.<&> (lecturer E.^. LecturerCourse)
E.<&> (lecturer E.^. LecturerType)
)
(\_current excluded -> [ LecturerType E.=. excluded E.^. LecturerType ])
deleteWhere [ LecturerUser ==. oldUserId ]
do
collision <- E.selectMaybe . E.from $ \(courseParticipantA `E.InnerJoin` courseParticipantB) -> do
E.on $ courseParticipantA E.^. CourseParticipantCourse E.==. courseParticipantB E.^. CourseParticipantCourse
E.&&. courseParticipantA E.^. CourseParticipantUser E.==. E.val oldUserId
E.&&. courseParticipantB E.^. CourseParticipantUser E.==. E.val newUserId
E.where_ . E.isJust $ courseParticipantA E.^. CourseParticipantAllocated
E.where_ . E.isJust $ courseParticipantB E.^. CourseParticipantAllocated
return (courseParticipantA, courseParticipantB)
whenIsJust collision $ \(oldParticipant, newParticipant)
-> tellError $ UserAssimilateCourseParticipantDifferentAllocation oldParticipant newParticipant
E.insertSelectWithConflict
UniqueParticipant
(E.from $ \courseParticipant -> do
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val oldUserId
return $ CourseParticipant
E.<# (courseParticipant E.^. CourseParticipantCourse)
E.<&> E.val newUserId
E.<&> (courseParticipant E.^. CourseParticipantRegistration)
E.<&> (courseParticipant E.^. CourseParticipantAllocated)
E.<&> (courseParticipant E.^. CourseParticipantState)
)
(\current excluded ->
[ CourseParticipantState E.=. E.exprLift min (current E.^. CourseParticipantState) (excluded E.^. CourseParticipantState)
, CourseParticipantRegistration E.=. E.max (current E.^. CourseParticipantRegistration) (excluded E.^. CourseParticipantRegistration)
, CourseParticipantAllocated E.=. E.alt (current E.^. CourseParticipantAllocated) (excluded E.^. CourseParticipantAllocated)
]
)
deleteWhere [ CourseParticipantUser ==. oldUserId ]
let getCourseUserNotes = selectSource [ CourseUserNoteUser ==. oldUserId ] []
upsertCourseUserNote (Entity oldCUNId oldCUN) = do
collision <- getBy $ UniqueCourseUserNote newUserId (courseUserNoteCourse oldCUN)
newCUNId <- case collision of
Nothing -> oldCUNId <$ update oldCUNId [ CourseUserNoteUser =. newUserId ]
Just (Entity newCUNId newCUN) -> newCUNId <$ update newCUNId [ CourseUserNoteNote =. ((<>) `on` courseUserNoteNote) oldCUN newCUN ]
when (newCUNId /= oldCUNId) $
updateWhere [CourseUserNoteEditNote ==. oldCUNId] [CourseUserNoteEditNote =. newCUNId]
delete oldCUNId
in runConduit $ getCourseUserNotes .| C.mapM_ upsertCourseUserNote
updateWhere [ CourseUserNoteEditUser ==. oldUserId ] [ CourseUserNoteEditUser =. newUserId ]
E.insertSelectWithConflict
UniqueCourseUserExamOfficeOptOut
(E.from $ \examOfficeOptOut -> do
E.where_ $ examOfficeOptOut E.^. CourseUserExamOfficeOptOutUser E.==. E.val oldUserId
return $ CourseUserExamOfficeOptOut
E.<# (examOfficeOptOut E.^. CourseUserExamOfficeOptOutCourse)
E.<&> E.val newUserId
E.<&> (examOfficeOptOut E.^. CourseUserExamOfficeOptOutSchool)
)
(\_current _excluded -> [])
deleteWhere [ CourseUserExamOfficeOptOutUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueUserFunction
(E.from $ \userFunction -> do
E.where_ $ userFunction E.^. UserFunctionUser E.==. E.val oldUserId
return $ UserFunction
E.<# E.val newUserId
E.<&> (userFunction E.^. UserFunctionSchool)
E.<&> (userFunction E.^. UserFunctionFunction)
)
(\_current _excluded -> [])
deleteWhere [ UserFunctionUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueUserSystemFunction
(E.from $ \userSystemFunction -> do
E.where_ $ userSystemFunction E.^. UserSystemFunctionUser E.==. E.val oldUserId
return $ UserSystemFunction
E.<# E.val newUserId
E.<&> (userSystemFunction E.^. UserSystemFunctionFunction)
E.<&> (userSystemFunction E.^. UserSystemFunctionManual)
E.<&> (userSystemFunction E.^. UserSystemFunctionIsOptOut)
)
(\current excluded -> [ UserSystemFunctionManual E.=. (current E.^. UserSystemFunctionManual E.||. excluded E.^. UserSystemFunctionManual), UserSystemFunctionIsOptOut E.=. (current E.^. UserSystemFunctionIsOptOut E.&&. excluded E.^. UserSystemFunctionIsOptOut) ])
deleteWhere [ UserSystemFunctionUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueUserExamOffice
(E.from $ \userExamOffice -> do
E.where_ $ userExamOffice E.^. UserExamOfficeUser E.==. E.val oldUserId
return $ UserExamOffice
E.<# E.val newUserId
E.<&> (userExamOffice E.^. UserExamOfficeField)
)
(\_current _excluded -> [])
deleteWhere [ UserExamOfficeUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueUserSchool
(E.from $ \userSchool -> do
E.where_ $ userSchool E.^. UserSchoolUser E.==. E.val oldUserId
return $ UserSchool
E.<# E.val newUserId
E.<&> (userSchool E.^. UserSchoolSchool)
E.<&> (userSchool E.^. UserSchoolIsOptOut)
)
(\current excluded -> [ UserSchoolIsOptOut E.=. (current E.^. UserSchoolIsOptOut E.&&. excluded E.^. UserSchoolIsOptOut) ])
deleteWhere [ UserSchoolUser ==. oldUserId ]
updateWhere [ UserGroupMemberUser ==. oldUserId, UserGroupMemberPrimary ==. Active ] [ UserGroupMemberUser =. newUserId ]
E.insertSelectWithConflict
UniqueUserGroupMember
(E.from $ \userGroupMember -> do
E.where_ $ userGroupMember E.^. UserGroupMemberUser E.==. E.val oldUserId
return $ UserGroupMember
E.<# (userGroupMember E.^. UserGroupMemberGroup)
E.<&> E.val newUserId
E.<&> (userGroupMember E.^. UserGroupMemberPrimary)
)
(\_current _excluded -> [])
deleteWhere [ UserGroupMemberUser ==. oldUserId ]
do
collisions <- E.select . E.from $ \(allocationUserA `E.InnerJoin` allocationUserB) -> do
E.on $ allocationUserA E.^. AllocationUserAllocation E.==. allocationUserB E.^. AllocationUserAllocation
E.&&. allocationUserA E.^. AllocationUserUser E.==. E.val oldUserId
E.&&. allocationUserB E.^. AllocationUserUser E.==. E.val newUserId
E.where_ $ allocationUserA E.^. AllocationUserPriority E.!=. allocationUserB E.^. AllocationUserPriority
return (allocationUserA, allocationUserB)
forM_ collisions $ \(oldAllocUser, newAllocUser)
-> tellWarning $ UserAssimilateAllocationUserDifferentPriority oldAllocUser newAllocUser
E.insertSelectWithConflict
UniqueAllocationUser
(E.from $ \allocationUser -> do
E.where_ $ allocationUser E.^. AllocationUserUser E.==. E.val oldUserId
return $ AllocationUser
E.<# (allocationUser E.^. AllocationUserAllocation)
E.<&> E.val newUserId
E.<&> (allocationUser E.^. AllocationUserTotalCourses)
E.<&> (allocationUser E.^. AllocationUserPriority)
)
(\current excluded -> [ AllocationUserTotalCourses E.=. E.max (current E.^. AllocationUserTotalCourses) (excluded E.^. AllocationUserTotalCourses) ])
deleteWhere [ AllocationUserUser ==. oldUserId ]
do
collisions <- E.select . E.from $ \(allocationDeregisterA `E.InnerJoin` allocationDeregisterB) -> do
E.on $ allocationDeregisterA E.^. AllocationDeregisterCourse E.==. allocationDeregisterB E.^. AllocationDeregisterCourse
E.&&. allocationDeregisterA E.^. AllocationDeregisterUser E.==. E.val oldUserId
E.&&. allocationDeregisterB E.^. AllocationDeregisterUser E.==. E.val newUserId
return (allocationDeregisterA, allocationDeregisterB)
forM_ collisions $ \(oldAllocationDeregister, newAllocationDeregister) ->
tellWarning $ UserAssimilateAllocationDeregisterDuplicateCourse oldAllocationDeregister newAllocationDeregister
updateWhere [ AllocationDeregisterUser ==. oldUserId ] [ AllocationDeregisterUser =. newUserId ]
E.insertSelectWithConflict
UniqueAllocationNotificationSetting
(E.from $ \allocNotifySetting -> do
E.where_ $ allocNotifySetting E.^. AllocationNotificationSettingUser E.==. E.val oldUserId
return $ AllocationNotificationSetting
E.<# E.val newUserId
E.<&> (allocNotifySetting E.^. AllocationNotificationSettingAllocation)
E.<&> (allocNotifySetting E.^. AllocationNotificationSettingIsOptOut)
)
(\current excluded -> [ AllocationNotificationSettingIsOptOut E.=. (current E.^. AllocationNotificationSettingIsOptOut E.||. excluded E.^. AllocationNotificationSettingIsOptOut) ])
deleteWhere [ AllocationNotificationSettingUser ==. oldUserId ]
do
collisions <- E.select . E.from $ \(examRegistrationA `E.InnerJoin` examRegistrationB) -> do
E.on $ examRegistrationA E.^. ExamRegistrationExam E.==. examRegistrationB E.^. ExamRegistrationExam
E.&&. examRegistrationA E.^. ExamRegistrationUser E.==. E.val oldUserId
E.&&. examRegistrationB E.^. ExamRegistrationUser E.==. E.val newUserId
E.where_ $ examRegistrationA E.^. ExamRegistrationOccurrence E.!=. examRegistrationB E.^. ExamRegistrationOccurrence
E.&&. E.isJust (examRegistrationA E.^. ExamRegistrationOccurrence)
E.&&. E.isJust (examRegistrationB E.^. ExamRegistrationOccurrence)
return (examRegistrationA, examRegistrationB)
forM_ collisions $ \(oldExamRegistration, newExamRegistration)
-> tellWarning $ UserAssimilateExamRegistrationDifferentOccurrence oldExamRegistration newExamRegistration
E.insertSelectWithConflict
UniqueExamRegistration
(E.from $ \examRegistration -> do
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val oldUserId
return $ ExamRegistration
E.<# (examRegistration E.^. ExamRegistrationExam)
E.<&> E.val newUserId
E.<&> (examRegistration E.^. ExamRegistrationOccurrence)
E.<&> (examRegistration E.^. ExamRegistrationTime)
)
(\current excluded -> [ ExamRegistrationOccurrence E.=. E.alt (current E.^. ExamRegistrationOccurrence) (excluded E.^. ExamRegistrationOccurrence), ExamRegistrationTime E.=. E.min (current E.^. ExamRegistrationTime) (excluded E.^. ExamRegistrationTime) ])
deleteWhere [ ExamRegistrationUser ==. oldUserId ]
do
collision <- E.selectMaybe . E.from $ \(examPartResultA `E.InnerJoin` examPartResultB) -> do
E.on $ examPartResultA E.^. ExamPartResultExamPart E.==. examPartResultB E.^. ExamPartResultExamPart
E.&&. examPartResultA E.^. ExamPartResultUser E.==. E.val oldUserId
E.&&. examPartResultB E.^. ExamPartResultUser E.==. E.val newUserId
E.where_ $ examPartResultA E.^. ExamPartResultResult E.!=. examPartResultB E.^. ExamPartResultResult
return (examPartResultA, examPartResultB)
whenIsJust collision $ \(oldExamPartResult, newExamPartResult)
-> tellError $ UserAssimilateExamPartResultDifferentResult oldExamPartResult newExamPartResult
E.insertSelectWithConflict
UniqueExamPartResult
(E.from $ \examPartResult -> do
E.where_ $ examPartResult E.^. ExamPartResultUser E.==. E.val oldUserId
return $ ExamPartResult
E.<# (examPartResult E.^. ExamPartResultExamPart)
E.<&> E.val newUserId
E.<&> (examPartResult E.^. ExamPartResultResult)
E.<&> (examPartResult E.^. ExamPartResultLastChanged)
)
(\current excluded -> [ ExamPartResultLastChanged E.=. E.max (current E.^. ExamPartResultLastChanged) (excluded E.^. ExamPartResultLastChanged) ])
deleteWhere [ ExamPartResultUser ==. oldUserId ]
do
collision <- E.selectMaybe . E.from $ \(examBonusA `E.InnerJoin` examBonusB) -> do
E.on $ examBonusA E.^. ExamBonusExam E.==. examBonusB E.^. ExamBonusExam
E.&&. examBonusA E.^. ExamBonusUser E.==. E.val oldUserId
E.&&. examBonusB E.^. ExamBonusUser E.==. E.val newUserId
E.where_ $ examBonusA E.^. ExamBonusBonus E.!=. examBonusB E.^. ExamBonusBonus
return (examBonusA, examBonusB)
whenIsJust collision $ \(oldExamBonus, newExamBonus)
-> tellError $ UserAssimilateExamBonusDifferentBonus oldExamBonus newExamBonus
E.insertSelectWithConflict
UniqueExamBonus
(E.from $ \examBonus -> do
E.where_ $ examBonus E.^. ExamBonusUser E.==. E.val oldUserId
return $ ExamBonus
E.<# (examBonus E.^. ExamBonusExam)
E.<&> E.val newUserId
E.<&> (examBonus E.^. ExamBonusBonus)
E.<&> (examBonus E.^. ExamBonusLastChanged)
)
(\current excluded -> [ ExamBonusLastChanged E.=. E.max (current E.^. ExamBonusLastChanged) (excluded E.^. ExamBonusLastChanged) ])
deleteWhere [ ExamBonusUser ==. oldUserId ]
let getExamResults = selectSource [ ExamResultUser ==. oldUserId ] []
upsertExamResult oldEREnt@(Entity oldERId oldER) = do
newER' <- getBy $ UniqueExamResult (examResultExam oldER) newUserId
newERId <- case newER' of
Just newEREnt@(Entity _ newER)
| ((/=) `on` examResultResult) newER oldER
-> tellError $ UserAssimilateExamResultDifferentResult oldEREnt newEREnt
Just (Entity newERId newER) -> newERId <$ update newERId
[ ExamResultLastChanged =. (max `on` examResultLastChanged) oldER newER
]
Nothing -> insert oldER
{ examResultUser = newUserId
}
updateWhere [ ExamOfficeResultSyncedResult ==. oldERId ] [ ExamOfficeResultSyncedResult =. newERId ]
delete oldERId
in runConduit $ getExamResults .| C.mapM_ upsertExamResult
let getExamCorrectors = selectSource [ ExamCorrectorUser ==. oldUserId ] []
upsertExamCorrector (Entity oldECId examCorrector) = do
Entity newECId _ <- upsert examCorrector{ examCorrectorUser = newUserId } []
E.insertSelectWithConflict
UniqueExamPartCorrector
(E.from $ \(examPartCorrector `E.InnerJoin` examCorrector') -> do
E.on $ examCorrector' E.^. ExamCorrectorId E.==. examPartCorrector E.^. ExamPartCorrectorCorrector
E.where_ $ examCorrector' E.^. ExamCorrectorUser E.==. E.val oldUserId
E.&&. examCorrector' E.^. ExamCorrectorExam E.==. E.val (examCorrectorExam examCorrector)
return $ ExamPartCorrector
E.<# (examPartCorrector E.^. ExamPartCorrectorPart)
E.<&> E.val newECId
)
(\_current _excluded -> [])
deleteWhere [ ExamPartCorrectorCorrector ==. oldECId ]
delete oldECId
in runConduit $ getExamCorrectors .| C.mapM_ upsertExamCorrector
let getQueuedJobs = selectSource [] []
updateQueuedJob (Entity jId QueuedJob{..}) = maybeT (return ()) $ do
(content' :: Job) <- hoistMaybe $ JSON.parseMaybe parseJSON queuedJobContent
let uContent' = set (typesUsing @JobChildren . filtered (== oldUserId)) newUserId content'
guard $ uContent' /= content'
lift $ update jId [ QueuedJobContent =. toJSON uContent' ]
in runConduit $ getQueuedJobs .| C.mapM_ updateQueuedJob
updateWhere [ SentMailRecipient ==. Just oldUserId ] [ SentMailRecipient =. Just newUserId ]
updateWhere [ SheetEditUser ==. oldUserId] [ SheetEditUser =. newUserId ]
let getSheetPseudonyms = selectSource [ SheetPseudonymUser ==. oldUserId ] []
upsertSheetPseudonym (Entity oldSPId oldSP) = do
collision <- existsBy $ UniqueSheetPseudonymUser (sheetPseudonymSheet oldSP) newUserId
if
| collision -> delete oldSPId
| otherwise -> update oldSPId [ SheetPseudonymUser =. newUserId ]
in runConduit $ getSheetPseudonyms .| C.mapM_ upsertSheetPseudonym
let getSheetCorrectors = selectSource [ SheetCorrectorUser ==. oldUserId ] []
upsertSheetCorrector (Entity oldSCId oldSheetCorrector) = do
collision <- getBy $ UniqueSheetCorrector newUserId (sheetCorrectorSheet oldSheetCorrector)
case collision of
Nothing -> update oldSCId [ SheetCorrectorUser =. newUserId ]
Just (Entity newSCId newSheetCorrector) -> do
update newSCId
[ SheetCorrectorLoad =. (sheetCorrectorLoad oldSheetCorrector <> sheetCorrectorLoad newSheetCorrector)
, SheetCorrectorState =. (min `on` sheetCorrectorState) oldSheetCorrector newSheetCorrector
]
delete oldSCId
in runConduit $ getSheetCorrectors .| C.mapM_ upsertSheetCorrector
do
collision <- E.selectMaybe . E.from $ \(personalisedSheetFileA `E.InnerJoin` personalisedSheetFileB) -> do
E.on $ personalisedSheetFileA E.^. PersonalisedSheetFileSheet E.==. personalisedSheetFileB E.^. PersonalisedSheetFileSheet
E.&&. personalisedSheetFileA E.^. PersonalisedSheetFileType E.==. personalisedSheetFileB E.^. PersonalisedSheetFileType
E.&&. personalisedSheetFileA E.^. PersonalisedSheetFileTitle E.==. personalisedSheetFileB E.^. PersonalisedSheetFileTitle
E.&&. personalisedSheetFileA E.^. PersonalisedSheetFileUser E.==. E.val oldUserId
E.&&. personalisedSheetFileB E.^. PersonalisedSheetFileUser E.==. E.val newUserId
E.where_ . E.not_ $ personalisedSheetFileA E.^. PersonalisedSheetFileContent `E.maybeEq` personalisedSheetFileB E.^. PersonalisedSheetFileContent
return (personalisedSheetFileA, personalisedSheetFileB)
whenIsJust collision $ \(oldPersonalisedSheetFile, newPersonalisedSheetFile)
-> tellError $ UserAssimilatePersonalisedSheetFileDifferentContent oldPersonalisedSheetFile newPersonalisedSheetFile
E.insertSelectWithConflict
UniquePersonalisedSheetFile
(E.from $ \personalisedSheetFile -> do
E.where_ $ personalisedSheetFile E.^. PersonalisedSheetFileUser E.==. E.val oldUserId
return $ PersonalisedSheetFile
E.<# (personalisedSheetFile E.^. PersonalisedSheetFileSheet)
E.<&> E.val newUserId
E.<&> (personalisedSheetFile E.^. PersonalisedSheetFileType)
E.<&> (personalisedSheetFile E.^. PersonalisedSheetFileTitle)
E.<&> (personalisedSheetFile E.^. PersonalisedSheetFileContent)
E.<&> (personalisedSheetFile E.^. PersonalisedSheetFileModified)
)
(\current excluded -> [ PersonalisedSheetFileModified E.=. E.max (current E.^. PersonalisedSheetFileModified) (excluded E.^. PersonalisedSheetFileModified) ])
deleteWhere [ PersonalisedSheetFileUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueTutor
(E.from $ \tutor -> do
E.where_ $ tutor E.^. TutorUser E.==. E.val oldUserId
return $ Tutor
E.<# (tutor E.^. TutorTutorial)
E.<&> E.val newUserId
)
(\_current _excluded -> [])
do
collision <- E.selectMaybe . E.from $ \((tutorialA `E.InnerJoin` tutorialParticipantA) `E.InnerJoin` (tutorialB `E.InnerJoin` tutorialParticipantB)) -> do
E.on $ tutorialParticipantB E.^. TutorialParticipantTutorial E.==. tutorialB E.^. TutorialId
E.on $ tutorialA E.^. TutorialCourse E.==. tutorialB E.^. TutorialCourse
E.&&. tutorialParticipantB E.^. TutorialParticipantUser E.==. E.val newUserId
E.&&. tutorialParticipantA E.^. TutorialParticipantUser E.==. E.val oldUserId
E.on $ tutorialParticipantA E.^. TutorialParticipantTutorial E.==. tutorialA E.^. TutorialId
E.where_ $ tutorialA E.^. TutorialId E.!=. tutorialB E.^. TutorialId
E.&&. tutorialA E.^. TutorialRegGroup E.==. tutorialB E.^. TutorialRegGroup
return (tutorialParticipantA, tutorialParticipantB)
whenIsJust collision $ \(tutorialUserA, tutorialUserB)
-> tellError $ UserAssimilateTutorialParticipantCollidingRegGroups tutorialUserA tutorialUserB
E.insertSelectWithConflict
UniqueTutorialParticipant
(E.from $ \tutorialParticipant -> do
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val oldUserId
return $ TutorialParticipant
E.<# (tutorialParticipant E.^. TutorialParticipantTutorial)
E.<&> E.val newUserId
)
(\_current _excluded -> [])
deleteWhere [ TutorialParticipantUser ==. oldUserId ]
E.insertSelectWithConflict
UniqueSystemMessageHidden
(E.from $ \systemMessageHidden -> do
E.where_ $ systemMessageHidden E.^. SystemMessageHiddenUser E.==. E.val oldUserId
return $ SystemMessageHidden
E.<# (systemMessageHidden E.^. SystemMessageHiddenMessage)
E.<&> E.val newUserId
E.<&> (systemMessageHidden E.^. SystemMessageHiddenTime)
)
(\current excluded -> [ SystemMessageHiddenTime E.=. E.max (current E.^. SystemMessageHiddenTime) (excluded E.^. SystemMessageHiddenTime) ])
deleteWhere [ SystemMessageHiddenUser ==. oldUserId ]
let getStudyFeatures = selectSource [ StudyFeaturesUser ==. oldUserId ] []
upsertStudyFeatures (Entity oldSFId oldStudyFeatures) = do
collision <- getBy $ UniqueStudyFeatures newUserId (studyFeaturesDegree oldStudyFeatures) (studyFeaturesField oldStudyFeatures) (studyFeaturesType oldStudyFeatures) (studyFeaturesSemester oldStudyFeatures)
case collision of
Nothing -> update oldSFId [ StudyFeaturesUser =. newUserId ]
Just (Entity newSFId newStudyFeatures) -> do
update newSFId
[ StudyFeaturesSuperField =. ((<|>) `on` studyFeaturesSuperField) newStudyFeatures oldStudyFeatures
, StudyFeaturesFirstObserved =. (min `on` studyFeaturesFirstObserved) oldStudyFeatures newStudyFeatures
, StudyFeaturesLastObserved =. (max `on` studyFeaturesLastObserved) oldStudyFeatures newStudyFeatures
, StudyFeaturesValid =. ((||) `on` studyFeaturesValid) oldStudyFeatures newStudyFeatures
, StudyFeaturesRelevanceCached =. ((||) `on` studyFeaturesRelevanceCached) oldStudyFeatures newStudyFeatures
]
E.insertSelectWithConflict
UniqueRelevantStudyFeatures
(E.from $ \relevantStudyFeatures -> do
E.where_ $ relevantStudyFeatures E.^. RelevantStudyFeaturesStudyFeatures E.==. E.val oldSFId
return $ RelevantStudyFeatures
E.<# (relevantStudyFeatures E.^. RelevantStudyFeaturesTerm)
E.<&> E.val newSFId
)
(\_current _excluded -> [])
deleteWhere [ RelevantStudyFeaturesStudyFeatures ==. oldSFId ]
delete oldSFId
in runConduit $ getStudyFeatures .| C.mapM_ upsertStudyFeatures
delete oldUserId
audit $ TransactionUserAssimilated newUserId oldUserId
where
tellWarning :: UserAssimilateExceptionReason -> ReaderT SqlBackend (WriterT (Set UserAssimilateException) Handler) ()
tellWarning = lift . tellPoint . UserAssimilateException oldUserId newUserId
tellError :: forall a. UserAssimilateExceptionReason -> ReaderT SqlBackend (WriterT (Set UserAssimilateException) Handler) a
tellError = throwM . UserAssimilateException oldUserId newUserId

View File

@ -89,17 +89,20 @@ import Control.Monad.Random.Class as Import (MonadRandom(..))
import Control.Monad.Morph as Import import Control.Monad.Morph as Import
import Control.Monad.Trans.Resource as Import (ReleaseKey) import Control.Monad.Trans.Resource as Import (ReleaseKey)
import Control.Monad.Trans.Reader as Import import Control.Monad.Trans.Reader as Import
( reader, runReader, mapReader, withReader ( runReader, mapReader, withReader
, mapReaderT, withReaderT , mapReaderT, withReaderT
) )
import Control.Monad.Reader.Class as Import (MonadReader(..))
import Control.Monad.Trans.State as Import import Control.Monad.Trans.State as Import
( state, State, runState, mapState, withState ( State, runState, mapState, withState
, StateT(..), mapStateT, withStateT , StateT(..), mapStateT, withStateT
) )
import Control.Monad.State.Class as Import (MonadState(state))
import Control.Monad.Trans.Writer.Lazy as Import import Control.Monad.Trans.Writer.Lazy as Import
( writer, Writer, runWriter, mapWriter, execWriter ( Writer, runWriter, mapWriter, execWriter
, WriterT(..), mapWriterT, execWriterT , WriterT(..), mapWriterT, execWriterT
) )
import Control.Monad.Writer.Class as Import (MonadWriter(..))
import Control.Monad.Trans.Except as Import import Control.Monad.Trans.Except as Import
( except, Except, runExcept, mapExcept ( except, Except, runExcept, mapExcept
, ExceptT(..), runExceptT, mapExceptT, throwE , ExceptT(..), runExceptT, mapExceptT, throwE
@ -144,7 +147,7 @@ import Data.Time.Clock.Instances as Import ()
import Data.Time.LocalTime.Instances as Import () import Data.Time.LocalTime.Instances as Import ()
import Data.Time.Calendar.Instances as Import () import Data.Time.Calendar.Instances as Import ()
import Data.Time.Format.Instances as Import () import Data.Time.Format.Instances as Import ()
import Network.Mail.Mime.Instances as Import () import Network.Mail.Mime.Instances as Import
import Yesod.Core.Instances as Import () import Yesod.Core.Instances as Import ()
import Data.Aeson.Types.Instances as Import () import Data.Aeson.Types.Instances as Import ()
import Database.Esqueleto.Instances as Import () import Database.Esqueleto.Instances as Import ()

View File

@ -32,7 +32,6 @@ import Data.Map.Strict ((!))
import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST, evalRWST) import Control.Monad.Trans.RWS.Lazy (RWST, mapRWST, evalRWST)
import Control.Monad.Trans.State.Strict (StateT, evalStateT) import Control.Monad.Trans.State.Strict (StateT, evalStateT)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Writer.Class (MonadWriter(..))
import Control.Monad.Trans.Cont (ContT(..), callCC) import Control.Monad.Trans.Cont (ContT(..), callCC)
import Control.Monad.Random.Lazy (evalRandTIO, mapRandT) import Control.Monad.Random.Lazy (evalRandTIO, mapRandT)
import Control.Monad.Logger import Control.Monad.Logger
@ -61,6 +60,7 @@ import Jobs.Handler.PruneInvitations
import Jobs.Handler.ChangeUserDisplayEmail import Jobs.Handler.ChangeUserDisplayEmail
import Jobs.Handler.Files import Jobs.Handler.Files
import Jobs.Handler.PersonalisedSheetFiles import Jobs.Handler.PersonalisedSheetFiles
import Jobs.Handler.PruneOldSentMails
import Jobs.HealthReport import Jobs.HealthReport

View File

@ -17,8 +17,6 @@ import Data.Time.Clock.POSIX
import Handler.Utils.DateTime import Handler.Utils.DateTime
import Control.Monad.Writer.Class (MonadWriter(..))
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -80,6 +78,16 @@ determineCrontab = execWriterT $ do
, cronNotAfter = Right CronNotScheduled , cronNotAfter = Right CronNotScheduled
} }
oldestSentMail <- lift $ preview (_head . _entityVal . _sentMailSentAt) <$> selectList [] [Asc SentMailSentAt, LimitTo 1]
whenIsJust ((,) <$> appMailRetainSent <*> oldestSentMail) $ \(retain, oldest) -> tell $ HashMap.singleton
(JobCtlQueue JobPruneOldSentMails)
Cron
{ cronInitial = CronTimestamp . utcToLocalTime $ addUTCTime retain oldest
, cronRepeat = CronRepeatOnChange
, cronRateLimit = retain / 2
, cronNotAfter = Right CronNotScheduled
}
whenIsJust (appInjectFiles <* appUploadCacheConf) $ \iInterval -> whenIsJust (appInjectFiles <* appUploadCacheConf) $ \iInterval ->
tell $ HashMap.singleton tell $ HashMap.singleton

View File

@ -0,0 +1,21 @@
module Jobs.Handler.PruneOldSentMails
( dispatchJobPruneOldSentMails
) where
import Import
import qualified Database.Esqueleto as E
import Database.Persist.Sql (deleteWhereCount)
dispatchJobPruneOldSentMails :: JobHandler UniWorX
dispatchJobPruneOldSentMails = JobHandlerAtomic $ do
retain' <- getsYesod $ view _appMailRetainSent
whenIsJust retain' $ \retain -> do
now <- liftIO getCurrentTime
del <- deleteWhereCount [SentMailSentAt <. addUTCTime (-retain) now]
$logInfoS "JobPruneOldSentMails" [st|Deleted #{del} old sent mails|]
del <- E.deleteCount . E.from $ \sentMailContent ->
E.where_ . E.not_ . E.exists . E.from $ \sentMail ->
E.where_ $ sentMail E.^. SentMailContentRef E.==. sentMailContent E.^. SentMailContentId
$logInfoS "JobPruneOldSentMails" [st|Deleted #{del} old sent mail bodies|]

View File

@ -23,10 +23,5 @@ import Jobs.Handler.SendNotification.SubmissionEdited
dispatchJobSendNotification :: UserId -> Notification -> JobHandler UniWorX dispatchJobSendNotification :: UserId -> Notification -> JobHandler UniWorX
dispatchJobSendNotification jRecipient jNotification = JobHandlerException $ do dispatchJobSendNotification jRecipient jNotification = JobHandlerException $
$(dispatchTH ''Notification) jNotification jRecipient $(dispatchTH ''Notification) jNotification jRecipient
instanceID' <- getsYesod $ view instanceID
now <- liftIO getCurrentTime
runDB . insert_ $ SentNotification (toJSON jNotification) jRecipient now instanceID'

View File

@ -63,6 +63,10 @@ dispatchHealthCheckMatchingClusterConfig
ourSetting <- getsYesod $ fmap fst . appMemcached ourSetting <- getsYesod $ fmap fst . appMemcached
dbSetting <- clusterSetting @'ClusterMemcachedKey dbSetting <- clusterSetting @'ClusterMemcachedKey
return $ maybe True ((== dbSetting) . Just) ourSetting return $ maybe True ((== dbSetting) . Just) ourSetting
clusterSettingMatches ClusterVerpSecret = do
ourSetting <- getsYesod appVerpSecret
dbSetting <- clusterSetting @'ClusterVerpSecret
return $ Just ourSetting == dbSetting
clusterSetting :: forall key. clusterSetting :: forall key.

View File

@ -13,8 +13,6 @@ import Import hiding ((<>))
import Jobs.Types import Jobs.Types
import Control.Monad.Writer.Class (MonadWriter(..))
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List.NonEmpty as NonEmpty

View File

@ -1,8 +1,9 @@
{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} {-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving, UndecidableInstances #-}
module Jobs.Types module Jobs.Types
( Job(..), Notification(..) ( Job(..), Notification(..)
, JobChildren
, classifyJob , classifyJob
, JobCtl(..) , JobCtl(..)
, classifyJobCtl , classifyJobCtl
@ -42,6 +43,8 @@ import Cron (CronNextMatch(..), _MatchAsap, _MatchAt, _MatchNone)
import System.Clock (getTime, Clock(Monotonic), TimeSpec) import System.Clock (getTime, Clock(Monotonic), TimeSpec)
import GHC.Conc (unsafeIOToSTM) import GHC.Conc (unsafeIOToSTM)
import Data.Generics.Product.Types (Children, ChGeneric)
data Job data Job
= JobSendNotification { jRecipient :: UserId, jNotification :: Notification } = JobSendNotification { jRecipient :: UserId, jNotification :: Notification }
@ -93,6 +96,7 @@ data Job
| JobPruneFallbackPersonalisedSheetFilesKeys | JobPruneFallbackPersonalisedSheetFilesKeys
| JobRechunkFiles | JobRechunkFiles
| JobDetectMissingFiles | JobDetectMissingFiles
| JobPruneOldSentMails
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)
data Notification data Notification
= NotificationSubmissionRated { nSubmission :: SubmissionId } = NotificationSubmissionRated { nSubmission :: SubmissionId }
@ -144,6 +148,24 @@ deriveJSON defaultOptions
, sumEncoding = TaggedObject "notification" "data" , sumEncoding = TaggedObject "notification" "data"
} ''Notification } ''Notification
data JobChildren
type instance Children JobChildren a = ChildrenJobChildren a
type family ChildrenJobChildren a where
ChildrenJobChildren ByteString = '[]
ChildrenJobChildren Html = '[]
ChildrenJobChildren Day = '[]
ChildrenJobChildren DiffTime = '[]
ChildrenJobChildren (SelDateTimeFormat -> DateTimeFormat) = '[]
ChildrenJobChildren Natural = '[]
ChildrenJobChildren UUID = '[]
ChildrenJobChildren (Key a) = '[]
ChildrenJobChildren (CI a) = '[]
ChildrenJobChildren (Set a) = '[]
ChildrenJobChildren a = Children ChGeneric a
classifyJob :: Job -> String classifyJob :: Job -> String
classifyJob job = unpack tag classifyJob job = unpack tag
where where

View File

@ -7,6 +7,7 @@ module Language.Haskell.TH.Instances
import ClassyPrelude import ClassyPrelude
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Lift (deriveLift) import Language.Haskell.TH.Lift (deriveLift)
import Data.Binary (Binary) import Data.Binary (Binary)
@ -15,6 +16,14 @@ instance Binary Loc
deriveLift ''Loc deriveLift ''Loc
instance Binary OccName
instance Binary ModName
instance Binary NameSpace
instance Binary PkgName
instance Binary NameFlavour
instance Binary Name
instance Semigroup (Q [Dec]) where instance Semigroup (Q [Dec]) where
(<>) = liftA2 (<>) (<>) = liftA2 (<>)

View File

@ -8,13 +8,12 @@ module Mail
module Network.Mail.Mime module Network.Mail.Mime
-- * MailT -- * MailT
, MailT, defMailT , MailT, defMailT
, MailSmtpData(..) , MailSmtpData(..), _smtpEnvelopeFrom, _smtpRecipients
, _MailSmtpDataSet , _MailSmtpDataSet
, MailContext(..) , MailContext(..)
, MonadMail(..) , MonadMail(..)
, getMailMessageRender, getMailMsgRenderer , getMailMessageRender, getMailMsgRenderer
-- * YesodMail -- * YesodMail
, VerpMode(..)
, YesodMail(..) , YesodMail(..)
, MailException(..) , MailException(..)
-- * Monadically constructing Mail -- * Monadically constructing Mail
@ -25,12 +24,13 @@ module Mail
, MonadHeader(..) , MonadHeader(..)
, MailHeader , MailHeader
, MailObjectId , MailObjectId
, replaceMailHeader, addMailHeader, removeMailHeader , replaceMailHeader, addMailHeader, removeMailHeader, getMailHeaders, lookupMailHeader
, replaceMailHeaderI, addMailHeaderI , replaceMailHeaderI, addMailHeaderI
, setSubjectI , setSubjectI
, setMailObjectUUID, setMailObjectIdRandom, setMailObjectIdCrypto, setMailObjectIdPseudorandom , setMailObjectUUID, setMailObjectIdRandom, setMailObjectIdCrypto, setMailObjectIdPseudorandom
, getMailObjectId
, setDate, setDateCurrent , setDate, setDateCurrent
, setMailSmtpData , getMailSmtpData
, _addressName, _addressEmail , _addressName, _addressEmail
, _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailHeader, _mailParts , _mailFrom, _mailTo, _mailCc, _mailBcc, _mailHeaders, _mailHeader, _mailParts
, _partType, _partEncoding, _partDisposition, _partFilename, _partHeaders, _partContent , _partType, _partEncoding, _partDisposition, _partFilename, _partHeaders, _partContent
@ -50,7 +50,7 @@ import Settings.Mime
import Data.Monoid (Last(..)) import Data.Monoid (Last(..))
import Control.Monad.Trans.RWS (RWST(..)) import Control.Monad.Trans.RWS (RWST(..))
import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT) import Control.Monad.Trans.State (StateT(..), execStateT, mapStateT)
import Control.Monad.Trans.Writer (execWriter, Writer) import Control.Monad.Trans.Writer (execWriter, execWriterT, Writer)
import Control.Monad.RWS.Class (MonadWriter(..), MonadState(..), modify) import Control.Monad.RWS.Class (MonadWriter(..), MonadState(..), modify)
import Control.Monad.Fail import Control.Monad.Fail
import Control.Monad.Base import Control.Monad.Base
@ -62,16 +62,15 @@ import qualified Data.Sequence as Seq
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Text as Text
import qualified Data.Foldable as Foldable import qualified Data.Foldable as Foldable
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LT import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Utils (MsgRendererS(..), MonadSecretBox(..), maybeT, YamlValue) import Utils (MsgRendererS(..), MonadSecretBox(..), YamlValue)
import Utils.Lens.TH import Utils.Lens.TH
import Control.Lens hiding (from) import Control.Lens hiding (from)
@ -109,8 +108,6 @@ import Data.Universe.Instances.Reverse ()
import Data.Universe.Instances.Reverse.JSON () import Data.Universe.Instances.Reverse.JSON ()
import Data.Universe.Instances.Reverse.Hashable () import Data.Universe.Instances.Reverse.Hashable ()
import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
@ -123,6 +120,8 @@ import Crypto.Hash.Algorithms (SHAKE128)
import Language.Haskell.TH (nameBase) import Language.Haskell.TH (nameBase)
import Network.Mail.Mime.Instances()
makeLenses_ ''Address makeLenses_ ''Address
makeLenses_ ''Mail makeLenses_ ''Mail
@ -152,7 +151,7 @@ instance {-# OVERLAPPING #-} (MonadCrypto m, MonadCryptoKey m ~ CryptoIDKey) =>
data MailSmtpData = MailSmtpData data MailSmtpData = MailSmtpData
{ smtpEnvelopeFrom :: Last Text { smtpEnvelopeFrom :: Last Text
, smtpRecipients :: Set Text , smtpRecipients :: Set Address
} deriving (Eq, Ord, Show, Read, Generic) } deriving (Eq, Ord, Show, Read, Generic)
instance Semigroup MailSmtpData where instance Semigroup MailSmtpData where
@ -188,6 +187,8 @@ instance Default MailContext where
makeLenses_ ''MailContext makeLenses_ ''MailContext
makeLenses_ ''MailSmtpData
class (MonadHandler m, MonadState Mail m) => MonadMail m where class (MonadHandler m, MonadState Mail m) => MonadMail m where
askMailLanguages :: m Languages askMailLanguages :: m Languages
askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat askMailDateTimeFormat :: SelDateTimeFormat -> m DateTimeFormat
@ -198,16 +199,6 @@ instance MonadHandler m => MonadMail (MailT m) where
askMailDateTimeFormat = (view _mcDateTimeFormat ??) askMailDateTimeFormat = (view _mcDateTimeFormat ??)
tellMailSmtpData = tell tellMailSmtpData = tell
data VerpMode = VerpNone
| Verp { verpSeparator, verpAtReplacement :: Char }
deriving (Eq, Show, Read, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, fieldLabelModifier = intercalate "-" . map toLower . drop 1 . splitCamel
, sumEncoding = UntaggedValue
} ''VerpMode
getMailMessageRender :: ( MonadMail m getMailMessageRender :: ( MonadMail m
, HandlerSite m ~ site , HandlerSite m ~ site
, RenderMessage site msg , RenderMessage site msg
@ -234,6 +225,8 @@ instance Exception MailException
class Yesod site => YesodMail site where class Yesod site => YesodMail site where
defaultFromAddress :: (MonadHandler m, HandlerSite m ~ site) => m Address defaultFromAddress :: (MonadHandler m, HandlerSite m ~ site) => m Address
defaultFromAddress = Address Nothing . ("yesod@" <>) . pack <$> liftIO getHostName defaultFromAddress = Address Nothing . ("yesod@" <>) . pack <$> liftIO getHostName
envelopeFromAddress :: (MonadHandler m, HandlerSite m ~ site) => m Text
envelopeFromAddress = addressEmail <$> defaultFromAddress
mailObjectIdDomain :: (MonadHandler m, HandlerSite m ~ site) => m Text mailObjectIdDomain :: (MonadHandler m, HandlerSite m ~ site) => m Text
mailObjectIdDomain = pack <$> liftIO getHostName mailObjectIdDomain = pack <$> liftIO getHostName
@ -248,11 +241,6 @@ class Yesod site => YesodMail site where
) => (SMTPConnection -> m a) -> m a ) => (SMTPConnection -> m a) -> m a
mailSmtp _ = throwM MailNotAvailable mailSmtp _ = throwM MailNotAvailable
mailVerp :: ( MonadHandler m
, HandlerSite m ~ site
) => m VerpMode
mailVerp = return VerpNone
mailT :: ( MonadHandler m mailT :: ( MonadHandler m
, HandlerSite m ~ site , HandlerSite m ~ site
, MonadUnliftIO m , MonadUnliftIO m
@ -304,7 +292,7 @@ defMailT ls (MailT mailC) = do
MailSmtpData{ smtpRecipients } MailSmtpData{ smtpRecipients }
| Set.null smtpRecipients -> throwM MailNoRecipientsSpecified | Set.null smtpRecipients -> throwM MailNoRecipientsSpecified
MailSmtpData{ smtpEnvelopeFrom = Last (Just (unpack -> returnPath)) MailSmtpData{ smtpEnvelopeFrom = Last (Just (unpack -> returnPath))
, smtpRecipients = (map unpack . toList -> recipients) , smtpRecipients = (map (unpack . addressEmail) . toList -> recipients)
} -> mailSmtp $ \conn -> do } -> mailSmtp $ \conn -> do
$logInfoS "Mail" $ "Submitting email: " <> tshow smtpData $logInfoS "Mail" $ "Submitting email: " <> tshow smtpData
liftIO $ SMTP.sendMail liftIO $ SMTP.sendMail
@ -434,15 +422,17 @@ partIsAttachment (repack -> fName) = modifyPart $ _partDisposition .= Attachment
class MonadHandler m => MonadHeader m where class MonadHandler m => MonadHeader m where
stateHeaders :: forall a. (Headers -> (a, Headers)) -> m a
modifyHeaders :: (Headers -> Headers) -> m () modifyHeaders :: (Headers -> Headers) -> m ()
modifyHeaders f = stateHeaders $ ((), ) . f
objectIdHeader :: m MailHeader objectIdHeader :: m MailHeader
instance MonadHandler m => MonadHeader (MailT m) where instance MonadHandler m => MonadHeader (MailT m) where
modifyHeaders f = MailT . modify $ over _mailHeaders f stateHeaders = MailT . zoom _mailHeaders . state
objectIdHeader = return "Message-ID" objectIdHeader = return "Message-ID"
instance MonadHandler m => MonadHeader (StateT Part m) where instance MonadHandler m => MonadHeader (StateT Part m) where
modifyHeaders f = _partHeaders %= f stateHeaders = zoom _partHeaders . state
objectIdHeader = return "Content-ID" objectIdHeader = return "Content-ID"
@ -459,6 +449,12 @@ addMailHeader header c = modifyHeaders $ \mailHeaders -> mailHeaders `snoc` (hea
removeMailHeader :: MonadHeader m => MailHeader -> m () removeMailHeader :: MonadHeader m => MailHeader -> m ()
removeMailHeader header = modifyHeaders $ \mailHeaders -> filter ((/= header) . fst) mailHeaders removeMailHeader header = modifyHeaders $ \mailHeaders -> filter ((/= header) . fst) mailHeaders
getMailHeaders :: MonadHeader m => MailHeader -> m [Text]
getMailHeaders header = stateHeaders $ \hdrs -> (, hdrs) . map (view _2) $ filter (views _1 (== header)) hdrs
lookupMailHeader :: MonadHeader m => MailHeader -> m (Maybe Text)
lookupMailHeader = fmap listToMaybe . getMailHeaders
replaceMailHeaderI :: ( RenderMessage site msg replaceMailHeaderI :: ( RenderMessage site msg
, MonadMail m , MonadMail m
@ -512,6 +508,11 @@ setMailObjectIdPseudorandom obj = do
seed = KMAC.finalize . KMAC.updates (KMAC.initialize (BS.pack . encodeUtf8 $ nameBase 'setMailObjectIdPseudorandom) $ Saltine.encode sbKey) . LBS.toChunks $ Binary.encode obj seed = KMAC.finalize . KMAC.updates (KMAC.initialize (BS.pack . encodeUtf8 $ nameBase 'setMailObjectIdPseudorandom) $ Saltine.encode sbKey) . LBS.toChunks $ Binary.encode obj
setMailObjectUUID . fromMaybe (error "Could not convert hash to UUID") . UUID.fromByteString $ fromStrict (ByteArray.convert seed :: ByteString) setMailObjectUUID . fromMaybe (error "Could not convert hash to UUID") . UUID.fromByteString $ fromStrict (ByteArray.convert seed :: ByteString)
getMailObjectId :: ( MonadHeader m, YesodMail (HandlerSite m) ) => m (Maybe MailObjectId)
getMailObjectId = fmap (fmap stripBrackets) . lookupMailHeader =<< objectIdHeader
where stripBrackets val = fromMaybe val $
Text.stripSuffix ">" =<< Text.stripPrefix "<" val
setDateCurrent :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m () setDateCurrent :: (MonadHandler m, YesodMail (HandlerSite m)) => MailT m ()
setDateCurrent = setDate =<< liftIO getCurrentTime setDateCurrent = setDate =<< liftIO getCurrentTime
@ -539,27 +540,12 @@ setDate time = do
] ]
setMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m () getMailSmtpData :: (MonadHandler m, YesodMail (HandlerSite m), MonadThrow m) => MailT m MailSmtpData
setMailSmtpData = do getMailSmtpData = execWriterT $ do
Address _ from <- maybeT (throwM MailNoSenderSpecified) $ asum from <- envelopeFromAddress
[ MaybeT . preuses (_mailHeader "Sender") $ fromString . unpack recps <- lift $ Set.fromList . concat <$> forM [_mailTo, _mailCc, _mailBcc] use
, use _mailFrom
]
recps <- Set.fromList . map addressEmail . concat <$> forM [_mailTo, _mailCc, _mailBcc] use
tell $ mempty { smtpRecipients = recps } tell $ mempty
{ smtpRecipients = recps
verpMode <- mailVerp , smtpEnvelopeFrom = Last $ Just from
if }
| Verp{..} <- verpMode
, [recp] <- Set.toList recps
-> let (user, domain) = Text.breakOn "@" from
verp = mconcat
[ user
, Text.singleton verpSeparator
, Text.replace "@" (Text.singleton verpAtReplacement) recp
, domain
]
in tell $ mempty { smtpEnvelopeFrom = Last $ Just verp }
| otherwise
-> tell $ mempty { smtpEnvelopeFrom = Last $ Just from }

View File

@ -176,6 +176,8 @@ migrateManual = do
, ("submission_edit_submission", "CREATE INDEX submission_edit_submission ON submission_edit (submission)" ) , ("submission_edit_submission", "CREATE INDEX submission_edit_submission ON submission_edit (submission)" )
, ("user_ldap_primary_key", "CREATE INDEX user_ldap_primary_key ON \"user\" (ldap_primary_key)" ) , ("user_ldap_primary_key", "CREATE INDEX user_ldap_primary_key ON \"user\" (ldap_primary_key)" )
, ("file_content_entry_chunk_hash", "CREATE INDEX file_content_entry_chunk_hash ON \"file_content_entry\" (chunk_hash)" ) , ("file_content_entry_chunk_hash", "CREATE INDEX file_content_entry_chunk_hash ON \"file_content_entry\" (chunk_hash)" )
, ("sent_mail_bounce_secret", "CREATE INDEX sent_mail_bounce_secret ON \"sent_mail\" (bounce_secret) WHERE bounce_secret IS NOT NULL")
, ("sent_mail_recipient", "CREATE INDEX sent_mail_recipient ON \"sent_mail\" (recipient) WHERE recipient IS NOT NULL")
] ]
where where
addIndex :: Text -> Sql -> Migration addIndex :: Text -> Sql -> Migration

View File

@ -1,5 +1,6 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-| {-|
Module: Model.Types.Mail Module: Model.Types.Mail
@ -17,6 +18,18 @@ import qualified Data.Aeson.Types as Aeson
import qualified Data.HashSet as HashSet import qualified Data.HashSet as HashSet
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import Crypto.Hash (digestFromByteString, SHAKE128)
import Database.Persist.Sql (PersistFieldSql)
import Data.ByteArray (ByteArrayAccess)
import qualified Data.ByteArray as BA
import Web.HttpApiData (ToHttpApiData, FromHttpApiData)
import Data.ByteString.Base32
import qualified Data.CaseInsensitive as CI
-- ^ `NotificationSettings` is for now a series of boolean checkboxes, i.e. a mapping @NotificationTrigger -> Bool@ -- ^ `NotificationSettings` is for now a series of boolean checkboxes, i.e. a mapping @NotificationTrigger -> Bool@
-- --
@ -85,3 +98,32 @@ instance FromJSON NotificationSettings where
return . NotificationSettings $ \n -> fromMaybe (notificationAllowed def n) $ HashMap.lookup n o' return . NotificationSettings $ \n -> fromMaybe (notificationAllowed def n) $ HashMap.lookup n o'
derivePersistFieldJSON ''NotificationSettings derivePersistFieldJSON ''NotificationSettings
newtype BounceSecret = BounceSecret (Digest (SHAKE128 64))
deriving (Eq, Ord, Read, Show, Lift, Generic, Typeable)
deriving newtype ( PersistField, PersistFieldSql
, Hashable, NFData
, ByteArrayAccess
)
instance PathPiece BounceSecret where
toPathPiece = CI.foldCase . encodeBase32Unpadded . BA.convert
fromPathPiece = fmap BounceSecret . digestFromByteString <=< either (const Nothing) Just . decodeBase32Unpadded . encodeUtf8
newtype MailContent = MailContent [Alternatives]
deriving (Eq, Show, Generic, Typeable)
deriving newtype (ToJSON, FromJSON)
deriving anyclass (Binary)
derivePersistFieldJSON ''MailContent
newtype MailContentReference = MailContentReference (Digest SHA3_512)
deriving (Eq, Ord, Read, Show, Lift, Generic, Typeable)
deriving newtype ( PersistField, PersistFieldSql
, PathPiece, ToHttpApiData, FromHttpApiData, ToJSON, FromJSON
, Hashable, NFData
, ByteArrayAccess
)
derivePersistFieldJSON ''MailHeaders

View File

@ -213,6 +213,16 @@ dnfAssumeValue var val
predDNFFalse :: PredDNF a predDNFFalse :: PredDNF a
predDNFFalse = PredDNF Set.empty predDNFFalse = PredDNF Set.empty
predDNFSingleton :: Ord a => PredLiteral a -> PredDNF a
predDNFSingleton = PredDNF . Set.singleton . impureNonNull . Set.singleton
predDNFAnd, predDNFOr :: Ord a => PredDNF a -> PredDNF a -> PredDNF a
predDNFAnd (PredDNF a) (PredDNF b) = PredDNF . Set.fromList $ do
aConj <- Set.toList a
bConj <- Set.toList b
return . impureNonNull $ toNullable aConj `Set.union` toNullable bConj
predDNFOr (PredDNF a) (PredDNF b) = PredDNF $ a <> b
data UserGroupName data UserGroupName
= UserGroupMetrics = UserGroupMetrics

View File

@ -302,16 +302,12 @@ instance Monoid Load where
data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused data CorrectorState = CorrectorNormal | CorrectorMissing | CorrectorExcused
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic)
deriving anyclass (Universe, Finite, Hashable)
deriveJSON defaultOptions deriveJSON defaultOptions
{ constructorTagModifier = fromJust . stripPrefix "Corrector" { constructorTagModifier = fromJust . stripPrefix "Corrector"
} ''CorrectorState } ''CorrectorState
instance Universe CorrectorState
instance Finite CorrectorState
instance Hashable CorrectorState
nullaryPathPiece ''CorrectorState (camelToPathPiece' 1) nullaryPathPiece ''CorrectorState (camelToPathPiece' 1)
derivePersistField "CorrectorState" derivePersistField "CorrectorState"

View File

@ -1,20 +1,31 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Network.Mail.Mime.Instances module Network.Mail.Mime.Instances
( ( MailHeaders(..)
) where ) where
import ClassyPrelude import ClassyPrelude
import Network.Mail.Mime import Network.Mail.Mime
import Data.Aeson import Data.Aeson (FromJSON(..), ToJSON(..))
import qualified Data.Aeson as Aeson
import Data.Aeson.TH import Data.Aeson.TH
import Control.Monad.Fail (MonadFail(..))
import Utils.PathPiece import Utils.PathPiece
import Utils (assertM) import Utils (assertM)
import qualified Data.Csv as Csv import qualified Data.Csv as Csv
import Data.Binary (Binary)
import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString as BS
import Data.Text.Encoding (decodeUtf8')
import Control.Lens
deriving instance Read Address deriving instance Read Address
@ -29,9 +40,9 @@ deriveToJSON defaultOptions
} ''Address } ''Address
instance FromJSON Address where instance FromJSON Address where
parseJSON = withObject "Address" $ \obj -> do parseJSON = Aeson.withObject "Address" $ \obj -> do
addressName <- assertM (not . null) <$> (obj .:? "name") addressName <- assertM (not . null) <$> (obj Aeson..:? "name")
addressEmail <- obj .: "email" addressEmail <- obj Aeson..: "email"
return Address{..} return Address{..}
@ -43,3 +54,68 @@ instance Csv.ToNamedRecord Address where
instance Csv.DefaultOrdered Address where instance Csv.DefaultOrdered Address where
headerOrder _ = Csv.header [ "name", "email" ] headerOrder _ = Csv.header [ "name", "email" ]
newtype MailHeaders = MailHeaders Headers
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance ToJSON MailHeaders where
toJSON (MailHeaders hs) = toJSON $ over (traverse . _1) decodeUtf8 hs
instance FromJSON MailHeaders where
parseJSON = fmap (MailHeaders . over (traverse . _1) encodeUtf8) . parseJSON
deriving instance Generic Encoding
deriving instance Generic Disposition
deriving instance Generic PartContent
deriving instance Generic Part
deriving instance Generic Mail
instance Binary Encoding
instance Binary Disposition
instance Binary PartContent
instance Binary Part
instance Binary Address
instance Binary Mail
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece
} ''Encoding
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece . dropSuffix "Disposition"
} ''Disposition
instance ToJSON PartContent where
toJSON (PartContent (toStrict -> content))
| BS.all (< 0x80) content
, Right content' <- decodeUtf8' content = Aeson.String content'
toJSON (PartContent content) = Aeson.object
[ "encoding" Aeson..= ("base64" :: String)
, "content" Aeson..= decodeUtf8 (Base64.encode $ toStrict content)
]
toJSON (NestedParts ps) = toJSON ps
instance FromJSON PartContent where
parseJSON (Aeson.String t) = return . PartContent . fromStrict $ encodeUtf8 t
parseJSON (Aeson.Object o) = do
encoding <- o Aeson..: "encoding"
content <- o Aeson..: "content"
if | encoding == "base64" -> either fail (return . PartContent . fromStrict) . Base64.decode $ encodeUtf8 content
| otherwise -> fail $ "Unknown encoding: “" <> encoding <> ""
parseJSON v = NestedParts <$> parseJSON v
instance ToJSON Part where
toJSON Part{..} = Aeson.object
[ "type" Aeson..= partType
, "encoding" Aeson..= partEncoding
, "disposition" Aeson..= partDisposition
, "headers" Aeson..= MailHeaders partHeaders
, "content" Aeson..= partContent
]
instance FromJSON Part where
parseJSON = Aeson.withObject "Part" $ \o -> do
partType <- o Aeson..: "type"
partEncoding <- o Aeson..: "encoding"
partDisposition <- o Aeson..: "disposition"
MailHeaders partHeaders <- o Aeson..: "headers"
partContent <- o Aeson..: "content"
return Part{..}

View File

@ -111,9 +111,12 @@ data AppSettings = AppSettings
, appSessionTokenExpiration :: Maybe NominalDiffTime , appSessionTokenExpiration :: Maybe NominalDiffTime
, appSessionTokenEncoding :: JwtEncoding , appSessionTokenEncoding :: JwtEncoding
, appMailFrom :: Address
, appMailObjectDomain :: Text , appMailObjectDomain :: Text
, appMailVerp :: VerpMode , appMailVerp :: VerpMode
, appMailRetainSent :: Maybe NominalDiffTime
, appMailEnvelopeFrom :: Text
, appMailFrom
, appMailSender
, appMailSupport :: Address , appMailSupport :: Address
, appJobWorkers :: Natural , appJobWorkers :: Natural
, appJobFlushInterval :: Maybe NominalDiffTime , appJobFlushInterval :: Maybe NominalDiffTime
@ -301,6 +304,16 @@ data TokenBucketConf = TokenBucketConf
, tokenBucketInitialValue :: Int64 , tokenBucketInitialValue :: Int64
} deriving (Eq, Ord, Show, Generic, Typeable) } deriving (Eq, Ord, Show, Generic, Typeable)
data VerpMode = VerpNone
| Verp { verpPrefix :: Text, verpSeparator :: Char }
deriving (Eq, Show, Read, Generic)
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, fieldLabelModifier = camelToPathPiece' 1
, sumEncoding = UntaggedValue
} ''VerpMode
deriveJSON defaultOptions deriveJSON defaultOptions
{ fieldLabelModifier = camelToPathPiece' 2 { fieldLabelModifier = camelToPathPiece' 2
} ''TokenBucketConf } ''TokenBucketConf
@ -449,9 +462,12 @@ instance FromJSON AppSettings where
appIpFromHeader <- o .: "ip-from-header" appIpFromHeader <- o .: "ip-from-header"
appMailFrom <- o .: "mail-from" appMailFrom <- o .: "mail-from"
appMailEnvelopeFrom <- o .:? "mail-envelope-from" .!= addressEmail appMailFrom
appMailSender <- o .:? "mail-sender" .!= appMailFrom
appMailSupport <- o .: "mail-support"
appMailObjectDomain <- o .: "mail-object-domain" appMailObjectDomain <- o .: "mail-object-domain"
appMailVerp <- fromMaybe VerpNone . join <$> (o .:? "mail-verp" <|> pure Nothing) appMailVerp <- fromMaybe VerpNone . join <$> (o .:? "mail-verp" <|> pure Nothing)
appMailSupport <- o .: "mail-support" appMailRetainSent <- o .: "mail-retain-sent"
appJobWorkers <- o .: "job-workers" appJobWorkers <- o .: "job-workers"
appJobFlushInterval <- o .:? "job-flush-interval" appJobFlushInterval <- o .:? "job-flush-interval"

View File

@ -3,6 +3,7 @@
module Settings.Cluster module Settings.Cluster
( ClusterSettingsKey(..) ( ClusterSettingsKey(..)
, ClusterSetting(..) , ClusterSetting(..)
, VerpSecret(..)
) where ) where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
@ -36,6 +37,9 @@ import Control.Monad.Fail
import Model.Types.TH.PathPiece import Model.Types.TH.PathPiece
import Data.ByteArray (ByteArray, ByteArrayAccess)
import qualified Crypto.Random as Crypto
data ClusterSettingsKey data ClusterSettingsKey
= ClusterCryptoIDKey = ClusterCryptoIDKey
@ -44,6 +48,7 @@ data ClusterSettingsKey
| ClusterJSONWebKeySet | ClusterJSONWebKeySet
| ClusterId | ClusterId
| ClusterMemcachedKey | ClusterMemcachedKey
| ClusterVerpSecret
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
instance Universe ClusterSettingsKey instance Universe ClusterSettingsKey
@ -131,3 +136,19 @@ instance ClusterSetting 'ClusterMemcachedKey where
type ClusterSettingValue 'ClusterMemcachedKey = AEAD.Key type ClusterSettingValue 'ClusterMemcachedKey = AEAD.Key
initClusterSetting _ = liftIO AEAD.newKey initClusterSetting _ = liftIO AEAD.newKey
knownClusterSetting _ = ClusterMemcachedKey knownClusterSetting _ = ClusterMemcachedKey
newtype VerpSecret = VerpSecret ByteString
deriving newtype (Eq, Ord, Monoid, Semigroup, ByteArray, ByteArrayAccess)
instance ToJSON VerpSecret where
toJSON (VerpSecret vSecret) = Aeson.String . decodeUtf8 $ Base64.encode vSecret
instance FromJSON VerpSecret where
parseJSON = Aeson.withText "VerpSecret" $ \t -> do
bytes <- either fail return . Base64.decode $ encodeUtf8 t
return $ VerpSecret bytes
instance ClusterSetting 'ClusterVerpSecret where
type ClusterSettingValue 'ClusterVerpSecret = VerpSecret
initClusterSetting _ = liftIO $ Crypto.getRandomBytes 16
knownClusterSetting _ = ClusterVerpSecret

View File

@ -822,7 +822,7 @@ and2M, or2M :: Monad m => m Bool -> m Bool -> m Bool
and2M ma mb = ifM ma mb (return False) and2M ma mb = ifM ma mb (return False)
or2M ma = ifM ma (return True) or2M ma = ifM ma (return True)
andM, orM :: (MonoFoldable mono, Element mono ~ (m Bool), Monad m) => mono -> m Bool andM, orM :: (MonoFoldable mono, Element mono ~ m Bool, Monad m) => mono -> m Bool
andM = ofoldl' and2M (return True) andM = ofoldl' and2M (return True)
orM = ofoldl' or2M (return False) orM = ofoldl' or2M (return False)
@ -887,9 +887,19 @@ diffTimeout timeoutLength timeoutRes act = fromMaybe timeoutRes <$> timeout time
= let (MkFixed micro :: Micro) = realToFrac timeoutLength = let (MkFixed micro :: Micro) = realToFrac timeoutLength
in fromInteger micro in fromInteger micro
------------
-- Writer --
------------
tellM :: (MonadTrans t, MonadWriter x (t m), Monad m) => m x -> t m () tellM :: (MonadTrans t, MonadWriter x (t m), Monad m) => m x -> t m ()
tellM = tell <=< lift tellM = tell <=< lift
tellPoint :: (MonadWriter mono m, MonoPointed mono) => Element mono -> m ()
tellPoint = tell . opoint
tellMPoint :: (MonadTrans t, MonadWriter mono (t m), Monad m, MonoPointed mono) => m (Element mono) -> t m ()
tellMPoint = tellM . fmap opoint
------------- -------------
-- Conduit -- -- Conduit --
------------- -------------
@ -1164,6 +1174,10 @@ setLastModified lastModified = do
safeMethods = [ methodGet, methodHead, methodOptions ] safeMethods = [ methodGet, methodHead, methodOptions ]
-- | Adapter for memoization of five-argument function
for5 :: (((k1, k2, k3, k4, k5) -> mv) -> (k1, k2, k3, k4, k5) -> mv) -> (k1 -> k2 -> k3 -> k4 -> k5 -> mv) -> k1 -> k2 -> k3 -> k4 -> k5 -> mv
for5 m f a b c d e = m (\(a',b',c',d',e') -> f a' b' c' d' e') (a,b,c,d,e)
-------------- --------------
-- Lattices -- -- Lattices --
-------------- --------------

View File

@ -16,7 +16,6 @@ import qualified Data.Array.MArray as MArr
import System.Random (RandomGen) import System.Random (RandomGen)
import Control.Monad.Trans.Random.Strict (evalRandT, RandT) import Control.Monad.Trans.Random.Strict (evalRandT, RandT)
import Control.Monad.Trans.State.Strict (StateT, modify', get, gets, evalStateT) import Control.Monad.Trans.State.Strict (StateT, modify', get, gets, evalStateT)
import Control.Monad.Writer (tell)
import Control.Monad.ST import Control.Monad.ST

View File

@ -224,7 +224,7 @@ data FormIdentifier
| FIDUserDelete | FIDUserDelete
| FIDCommunication | FIDCommunication
| FIDAssignSubmissions | FIDAssignSubmissions
| FIDUserAuthMode | FIDUserAuthMode | FIDUserAssimilate | FIDUserRights | FIDUserAuthentication
| FIDAllUsersAction | FIDAllUsersAction
| FIDLanguage | FIDLanguage
| FIDExamAutoOccurrenceCalculate | FIDExamAutoOccurrenceConfirm | FIDExamAutoOccurrenceNudge UUID | FIDExamAutoOccurrenceCalculate | FIDExamAutoOccurrenceConfirm | FIDExamAutoOccurrenceNudge UUID

View File

@ -260,6 +260,7 @@ makeLenses_ ''WorkflowGraphEdge
makePrisms ''WorkflowGraphEdge makePrisms ''WorkflowGraphEdge
makeWrapped ''Textarea makeWrapped ''Textarea
makeLenses_ ''SentMail
makePrisms ''AllocationPriority makePrisms ''AllocationPriority

View File

@ -57,7 +57,7 @@ data GlobalPostParam = PostFormIdentifier
| PostDeleteTarget | PostDeleteTarget
| PostMassInputShape | PostMassInputShape
| PostBearer | PostBearer
| PostDBCsvImportAction | PostDBCsvImportAction | PostDBCsvImportAvailableActions
| PostDBCsvReImport | PostDBCsvReImport
| PostLoginDummy | PostLoginDummy
| PostExamAutoOccurrencePrevious | PostExamAutoOccurrencePrevious

View File

@ -11,7 +11,11 @@ $newline never
_{MsgAdminUserAuthHeading} _{MsgAdminUserAuthHeading}
^{authForm} ^{authForm}
<section> <section>
<p> <h3>
_{MsgUserAccountDeleteWarning} _{MsgAdminUserAssimilate}
<p> ^{assimilateForm}
^{modal "Benutzer löschen" (Right deleteWidget)} $# <section>
$# <p>
$# _{MsgUserAccountDeleteWarning}
$# <p>
$# ^{modal "Benutzer löschen" (Right deleteWidget)}

View File

@ -225,13 +225,9 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
$if registrationOpen $if registrationOpen
$# regForm is defined through templates/widgets/registerForm $# regForm is defined through templates/widgets/registerForm
^{regForm} ^{regForm}
$if isJust mApplication && courseApplicationsRequired course $if (isJust mApplication && courseApplicationsRequired course) && mayReRegister
<p> <p .explanation>
_{MsgCourseApplicationDeleteToEdit} _{MsgCourseApplicationDeleteToEdit}
$else
$if isJust registration
<p>
_{MsgCourseRegistrationDeleteToEdit}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgCourseMaterial} _{MsgCourseMaterial}

View File

@ -1,5 +1,6 @@
$newline never $newline never
#{csrf} #{csrf}
^{availableActs}
<div .actions> <div .actions>
$forall actionClass <- sortOn dbtCsvCoarsenActionClass (Map.keys actionMap) $forall actionClass <- sortOn dbtCsvCoarsenActionClass (Map.keys actionMap)
<div .action> <div .action>

View File

@ -5,6 +5,10 @@ $newline never
_{MsgIdent} _{MsgIdent}
<dd .deflist__dd .email> <dd .deflist__dd .email>
#{userIdent} #{userIdent}
<dt .deflist__dt>
_{MsgAuthMode}
<dd .deflist__dd>
_{userAuthentication}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgName} _{MsgName}
<dd .deflist__dd> <dd .deflist__dd>
@ -18,19 +22,28 @@ $newline never
_{MsgEMail} _{MsgEMail}
<dd .deflist__dd> <dd .deflist__dd>
#{mailtoHtml userEmail} #{mailtoHtml userEmail}
$if userEmail /= userDisplayEmail
<dt .deflist__dt>
_{MsgUserDisplayEmail}
<dd .deflist__dd .email>
#{userDisplayEmail}
$if showAdminInfo $if showAdminInfo
<dt .deflist__dt>
_{MsgUserCreated}
<dd .deflist__dd>
^{formatTimeW SelFormatDateTime userCreated}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgLastLogin} _{MsgLastLogin}
<dd .deflist__dd> <dd .deflist__dd>
$maybe llogin <- lastLogin $maybe llogin <- userLastAuthentication
#{llogin} ^{formatTimeW SelFormatDateTime llogin}
$nothing $nothing
_{MsgNever} _{MsgNever}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgProfileLastLdapSynchronisation} _{MsgProfileLastLdapSynchronisation}
<dd .deflist__dd> <dd .deflist__dd>
$maybe lsync <- lastLdapSync $maybe lsync <- userLastLdapSynchronisation
#{lsync} ^{formatTimeW SelFormatDateTime lsync}
$nothing $nothing
_{MsgNever} _{MsgNever}
$maybe pKey <- userLdapPrimaryKey $maybe pKey <- userLdapPrimaryKey
@ -38,6 +51,13 @@ $newline never
_{MsgProfileLdapPrimaryKey} _{MsgProfileLdapPrimaryKey}
<dd .deflist__dd .ldap-primary-key> <dd .deflist__dd .ldap-primary-key>
#{pKey} #{pKey}
<dt .deflist__dt>
_{MsgTokensLastReset}
<dd .deflist__dd>
$maybe lastInvalidated <- userTokensIssuedAfter
^{formatTimeW SelFormatDateTime lastInvalidated}
$nothing
_{MsgNever}
$forall (function, schools) <- Map.toList functions $forall (function, schools) <- Map.toList functions
<dt .deflist__dt>_{function} <dt .deflist__dt>_{function}
<dd .deflist__dd> <dd .deflist__dd>

View File

@ -14,7 +14,7 @@ instance Arbitrary MailContext where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink
instance Arbitrary VerpMode where instance Arbitrary Address where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink
@ -25,5 +25,3 @@ spec = do
[ eqLaws, ordLaws, showReadLaws, monoidLaws ] [ eqLaws, ordLaws, showReadLaws, monoidLaws ]
lawsCheckHspec (Proxy @MailContext) lawsCheckHspec (Proxy @MailContext)
[ eqLaws, ordLaws, showReadLaws, jsonLaws, hashableLaws ] [ eqLaws, ordLaws, showReadLaws, jsonLaws, hashableLaws ]
lawsCheckHspec (Proxy @VerpMode)
[ eqLaws, showReadLaws, jsonLaws ]

View File

@ -5,9 +5,10 @@ module ModelSpec where
import TestImport import TestImport
import Settings (getTimeLocale') import Settings (getTimeLocale', VerpMode(..))
import Model.TypesSpec () import Model.TypesSpec ()
import MailSpec ()
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.ByteString.Char8 as CBS import qualified Data.ByteString.Char8 as CBS
@ -170,6 +171,11 @@ instance {-# OVERLAPS #-} (HasCryptoID ns ct pt (ReaderT CryptoIDKey Catch), Arb
where where
tmpKey = unsafePerformIO genKey tmpKey = unsafePerformIO genKey
instance Arbitrary VerpMode where
arbitrary = genericArbitrary
shrink = genericShrink
spec :: Spec spec :: Spec
spec = do spec = do
parallel $ do parallel $ do
@ -179,3 +185,5 @@ spec = do
[ eqLaws ] [ eqLaws ]
lawsCheckHspec (Proxy @Term) lawsCheckHspec (Proxy @Term)
[ eqLaws, jsonLaws ] [ eqLaws, jsonLaws ]
lawsCheckHspec (Proxy @VerpMode)
[ eqLaws, showReadLaws, jsonLaws ]