feat(forms): improve field labeling & error reporting

Fixes #588
This commit is contained in:
Gregor Kleen 2020-05-19 10:21:42 +02:00
parent 02e8825cba
commit 3820b45b3e
82 changed files with 365 additions and 251 deletions

View File

@ -3,44 +3,41 @@ import { Utility } from '../../core/utility';
const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized'; const FORM_ERROR_REMOVER_INITIALIZED_CLASS = 'form-error-remover--initialized';
const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select'; const FORM_ERROR_REMOVER_INPUTS_SELECTOR = 'input:not([type="hidden"]), textarea, select';
const FORM_GROUP_SELECTOR = '.form-group'; const FORM_GROUP_WITH_ERRORS_CLASSES = ['form-group--has-error', 'standalone-field--has-error'];
const FORM_GROUP_WITH_ERRORS_CLASS = 'form-group--has-error'; const FORM_GROUP_SELECTOR = FORM_GROUP_WITH_ERRORS_CLASSES.map(c => '.' + c).join(', ');
@Utility({ @Utility({
selector: 'form', selector: FORM_GROUP_SELECTOR,
}) })
export class FormErrorRemover { export class FormErrorRemover {
_element;
constructor(element) { constructor(element) {
if (!element) { if (!element)
throw new Error('Form Error Remover utility needs to be passed an element!'); throw new Error('Form Error Remover utility needs to be passed an element!');
}
if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS)) { if (element.classList.contains(FORM_ERROR_REMOVER_INITIALIZED_CLASS))
return false; return;
}
// find form groups if (FORM_GROUP_WITH_ERRORS_CLASSES.every(c => !element.classList.contains(c)))
const formGroups = Array.from(element.querySelectorAll(FORM_GROUP_SELECTOR)); return;
formGroups.forEach((formGroup) => { this._element = element;
if (!formGroup.classList.contains(FORM_GROUP_WITH_ERRORS_CLASS)) {
return; this._element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
} }
const inputElements = Array.from(formGroup.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR)); start() {
if (!inputElements) { if (!this._element)
return false; return;
}
inputElements.forEach((inputElement) => { const inputElements = Array.from(this._element.querySelectorAll(FORM_ERROR_REMOVER_INPUTS_SELECTOR));
inputElement.addEventListener('input', () => {
formGroup.classList.remove(FORM_GROUP_WITH_ERRORS_CLASS); inputElements.forEach((inputElement) => {
}); inputElement.addEventListener('input', () => {
FORM_GROUP_WITH_ERRORS_CLASSES.forEach(c => { this._element.classList.remove(c); });
}); });
}); });
// mark initialized
element.classList.add(FORM_ERROR_REMOVER_INITIALIZED_CLASS);
} }
} }

View File

@ -78,10 +78,32 @@
font-weight: 600 font-weight: 600
color: var(--color-error) color: var(--color-error)
margin: 7px 0 margin: 7px 0
white-space: pre-wrap
.form-error .form-error
display: none display: none
.standalone-field
display: inline-flex
&__error
display: none
align-self: center
flex: 0 0 auto
.tooltip__content
font-weight: 600
color: var(--color-error)
white-space: pre-wrap
&--has-error
input, textarea
border-color: var(--color-error) !important
.standalone-field__error
display: block
@media (max-width: 768px) @media (max-width: 768px)
.form-group .form-group
grid-template-columns: 1fr grid-template-columns: 1fr

View File

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

View File

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

View File

@ -91,6 +91,7 @@ TermEndDay: Letzter Tag
TermEndDayTooltip: Üblicherweise immer 30. September oder 31. März TermEndDayTooltip: Üblicherweise immer 30. September oder 31. März
TermHolidays: Feiertage TermHolidays: Feiertage
TermHolidayPlaceholder: Feiertag TermHolidayPlaceholder: Feiertag
TermHolidayMissing: Feiertag wird benötigt
TermLectureStart: Beginn Vorlesungen TermLectureStart: Beginn Vorlesungen
TermLectureEnd: Ende Vorlesungen TermLectureEnd: Ende Vorlesungen
TermLectureEndTooltip: Meistens dauer das Sommersemester 14 Wochen und das Wintersemester 15 Wochen. TermLectureEndTooltip: Meistens dauer das Sommersemester 14 Wochen und das Wintersemester 15 Wochen.
@ -257,6 +258,7 @@ CourseFormSectionRegistration: Anmeldung zum Kurs
CourseFormSectionAdministration: Verwaltung CourseFormSectionAdministration: Verwaltung
CourseLecturers: Kursverwalter CourseLecturers: Kursverwalter
CourseLecturerEmail: E-Mail
CourseLecturer: Dozent CourseLecturer: Dozent
CourseAssistant: Assistent CourseAssistant: Assistent
CourseLecturerAlreadyAdded: Dieser Nutzer ist bereits als Kursverwalter eingetragen CourseLecturerAlreadyAdded: Dieser Nutzer ist bereits als Kursverwalter eingetragen
@ -775,6 +777,8 @@ CampusUserInvalidSex: Konnte anhand des Campus-Logins kein Geschlecht ermitteln
CorrectorNormal: Normal CorrectorNormal: Normal
CorrectorMissing: Abwesend CorrectorMissing: Abwesend
CorrectorExcused: Entschuldigt CorrectorExcused: Entschuldigt
SheetCorrectorState: Status
SheetCorrectorProportion: Anteil
CorrectorStateTip: Abwesende Korrektoren bekommen bei späteren Übungsblättern mehr Korrekturen zum Ausgleich zugewiesen. Entschuldigte Korrektoren müssen nicht nacharbeiten. CorrectorStateTip: Abwesende Korrektoren bekommen bei späteren Übungsblättern mehr Korrekturen zum Ausgleich zugewiesen. Entschuldigte Korrektoren müssen nicht nacharbeiten.
DayIsAHoliday tid@TermId name@Text date@Text: "#{name}" (#{date}) ist ein Feiertag DayIsAHoliday tid@TermId name@Text date@Text: "#{name}" (#{date}) ist ein Feiertag
@ -843,6 +847,7 @@ NoStudyField: Kein Studienfach
StudyFeatureType: StudyFeatureType:
StudyFeatureValid: Aktiv StudyFeatureValid: Aktiv
StudyFeatureUpdate: Abgeglichen StudyFeatureUpdate: Abgeglichen
StudyTermsParentMissing: Elter wird benötigt
DegreeKey: Abschlussschlüssel DegreeKey: Abschlussschlüssel
DegreeName: Abschluss DegreeName: Abschluss
@ -1510,6 +1515,7 @@ SubmissionUserInviteExplanation: Sie wurden eingeladen, Mitabgebende(r) bei eine
ExamOfficeUserInviteHeading displayName@Text: Zugriff auf Ihre Prüfungsleistungen durch #{displayName} ExamOfficeUserInviteHeading displayName@Text: Zugriff auf Ihre Prüfungsleistungen durch #{displayName}
ExamOfficeUserInviteExplanation: Um Ihre Prüfungsleistungen ordnungsgemäß anrechnen zu können (z.B. im finalen Transcript of Records für Erasmus-Studierende) werden sie eingeladen der hierfür zuständigen Stelle Einsicht zu gewähren. ExamOfficeUserInviteExplanation: Um Ihre Prüfungsleistungen ordnungsgemäß anrechnen zu können (z.B. im finalen Transcript of Records für Erasmus-Studierende) werden sie eingeladen der hierfür zuständigen Stelle Einsicht zu gewähren.
ExamOfficeUserInvitationAccepted: Einsicht erfolgreich gewährt ExamOfficeUserInvitationAccepted: Einsicht erfolgreich gewährt
ExamOfficeUserEmail: E-Mail
InvitationAction: Aktion InvitationAction: Aktion
InvitationActionTip: Abgelehnte Einladungen können nicht mehr angenommen werden InvitationActionTip: Abgelehnte Einladungen können nicht mehr angenommen werden
@ -1576,6 +1582,7 @@ TutorialRegGroupTip: Studenten können sich in jeweils maximal einem Tutorium pr
TutorialRoomPlaceholder: Raum TutorialRoomPlaceholder: Raum
TutorialTutors: Tutoren TutorialTutors: Tutoren
TutorialTutorAlreadyAdded: Ein Tutor mit dieser E-Mail ist bereits für dieses Tutorium eingetragen TutorialTutorAlreadyAdded: Ein Tutor mit dieser E-Mail ist bereits für dieses Tutorium eingetragen
TutorEmail: E-Mail
OccurrenceNoneScheduled: (Noch) keine planmäßigen Termine OccurrenceNoneScheduled: (Noch) keine planmäßigen Termine
OccurrenceNoneExceptions: (Noch) keine Termin-Ausnahmen OccurrenceNoneExceptions: (Noch) keine Termin-Ausnahmen
@ -1701,6 +1708,7 @@ ExamNoSuchOccurrence: Termin/Raum existiert nicht (mehr)
ExamOccurrences: Termine ExamOccurrences: Termine
ExamRooms: Räume ExamRooms: Räume
ExamTimes: Termine ExamTimes: Termine
ExamRoomRoom: Raum
ExamRoomAlreadyExists: Prüfung ist bereits eingetragen ExamRoomAlreadyExists: Prüfung ist bereits eingetragen
ExamRoomName: Interne Bezeichnung ExamRoomName: Interne Bezeichnung
ExamRoomCapacity: Kapazität ExamRoomCapacity: Kapazität
@ -1724,6 +1732,7 @@ ExamFormParts: Teile
ExamCorrectors: Korrektoren ExamCorrectors: Korrektoren
ExamCorrectorsTip: Hier eingetragene Korrektoren können zwischen Beginn der Prüfung und "Bewertung abgeschlossen ab" Ergebnisse für alle Teilprüfungen und alle Teilnehmer im System hinterlegen. ExamCorrectorsTip: Hier eingetragene Korrektoren können zwischen Beginn der Prüfung und "Bewertung abgeschlossen ab" Ergebnisse für alle Teilprüfungen und alle Teilnehmer im System hinterlegen.
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen
ExamCorrectorEmail: E-Mail
ExamPart: Teilprüfung/Aufgabe ExamPart: Teilprüfung/Aufgabe
ExamParts: Teilprüfungen/Aufgaben ExamParts: Teilprüfungen/Aufgaben
@ -2109,6 +2118,7 @@ SchoolShort: Kürzel
SchoolName: Name SchoolName: Name
SchoolLdapOrganisations: Assoziierte LDAP-Fragmente SchoolLdapOrganisations: Assoziierte LDAP-Fragmente
SchoolLdapOrganisationsTip: Beim Login via LDAP werden dem Nutzer alle Institute zugeordnet deren assoziierte LDAP-Fragmente im Eintrag des Nutzer gefunden werden SchoolLdapOrganisationsTip: Beim Login via LDAP werden dem Nutzer alle Institute zugeordnet deren assoziierte LDAP-Fragmente im Eintrag des Nutzer gefunden werden
SchoolLdapOrganisationMissing: LDAP-Fragment wird benötigt
SchoolUpdated ssh@SchoolId: #{ssh} erfolgreich angepasst SchoolUpdated ssh@SchoolId: #{ssh} erfolgreich angepasst
SchoolTitle ssh@SchoolId: Institut „#{ssh}“ SchoolTitle ssh@SchoolId: Institut „#{ssh}“
@ -2428,6 +2438,7 @@ ExternalExamExamOfficeSchoolsTip: Prüfungsbeauftragte von Instituten, die Sie h
ExternalExamStaff: Assoziierte Personen ExternalExamStaff: Assoziierte Personen
ExternalExamStaffTip: Assoziierte Personen werden den Prüfungsbeauftragten und Teilnehmern angezeigt und dürfen Leistungen für die Prüfung hinterlegen. ExternalExamStaffTip: Assoziierte Personen werden den Prüfungsbeauftragten und Teilnehmern angezeigt und dürfen Leistungen für die Prüfung hinterlegen.
ExternalExamStaffAlreadyAdded: Person wurde bereits der Prüfung hinzugefügt ExternalExamStaffAlreadyAdded: Person wurde bereits der Prüfung hinzugefügt
ExternalExamStaffEmail: E-Mail
ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, für die externen Prüfungen, die Sie anlegen ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, für die externen Prüfungen, die Sie anlegen
ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden. ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden.
ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits. ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits.
@ -2530,13 +2541,17 @@ CourseOption tid@TermId ssh@SchoolId coursen@CourseName: #{tid} - #{ssh} - #{cou
BearerTokenUsageWarning: Mit diesem Interface können quesi beliebige Rechte als Tokens kodiert und somit ohne wesentliche weitere Beschränkung frei übertragen werden. Benutzen Sie dieses Interface nur, wenn Sie von einem erfahrenen Entwickler über die Auswirkungen des konkreten Tokens, dass sie ausstellen möchten, beraten wurden! BearerTokenUsageWarning: Mit diesem Interface können quesi beliebige Rechte als Tokens kodiert und somit ohne wesentliche weitere Beschränkung frei übertragen werden. Benutzen Sie dieses Interface nur, wenn Sie von einem erfahrenen Entwickler über die Auswirkungen des konkreten Tokens, dass sie ausstellen möchten, beraten wurden!
BearerTokenAuthorityGroups: Token-Authorität (Gruppen) BearerTokenAuthorityGroups: Token-Authorität (Gruppen)
BearerTokenAuthorityGroupsTip: Die primären Benutzer aller angegebenen Gruppen müssen Zugriff auf eine Route haben, damit das Token den Zugriff auf diese Route erlaubt. BearerTokenAuthorityGroupsTip: Die primären Benutzer aller angegebenen Gruppen müssen Zugriff auf eine Route haben, damit das Token den Zugriff auf diese Route erlaubt.
BearerTokenAuthorityGroupMissing: Gruppe wird benötigt
BearerTokenAuthorityUsers: Token-Authorität (Benutzer) BearerTokenAuthorityUsers: Token-Authorität (Benutzer)
BearerTokenAuthorityUsersTip: Alle angegebenen Benutzer müssen Zugriff auf eine Route haben, damit das Token den Zugriff auf diese Route erlaubt. Der Aussteller muss, bei mit diesem Benutzerinterface erzeugten Tokens, auch Zugriff auf die Route haben (er wird automatisch der Menge von Token-Authoritäten hinzugefügt). BearerTokenAuthorityUsersTip: Alle angegebenen Benutzer müssen Zugriff auf eine Route haben, damit das Token den Zugriff auf diese Route erlaubt. Der Aussteller muss, bei mit diesem Benutzerinterface erzeugten Tokens, auch Zugriff auf die Route haben (er wird automatisch der Menge von Token-Authoritäten hinzugefügt).
BearerTokenAuthorityUnknownUser email@UserEmail: Ein Nutzer mit E-Mail #{email} ist dem System nicht bekannt BearerTokenAuthorityUnknownUser email@UserEmail: Ein Nutzer mit E-Mail #{email} ist dem System nicht bekannt
BearerTokenRoutes: Erlaubte Routen BearerTokenRoutes: Erlaubte Routen
BearerTokenRoutesTip: Wenn die Token-Validität nach Routen eingeschränkt und keine Routen angegeben werden, ist das Token nirgends gültig. BearerTokenRoutesTip: Wenn die Token-Validität nach Routen eingeschränkt und keine Routen angegeben werden, ist das Token nirgends gültig.
BearerTokenRouteMissing: Route wird benötigt
BearerTokenRestrictions: Routen-spezifische Einschränkungen BearerTokenRestrictions: Routen-spezifische Einschränkungen
BearerTokenRestrictRoutes: Token-Validität nach Routen einschränken BearerTokenRestrictRoutes: Token-Validität nach Routen einschränken
BearerTokenRestrictRoute: Route
BearerTokenRestrictValue: Einschränkung
BearerTokenAdditionalAuth: Zusätzliche Authorisierung BearerTokenAdditionalAuth: Zusätzliche Authorisierung
BearerTokenAdditionalAuthTip: Wird hier nichts angegeben, werden keine Einschränkungen daran gesetzt, wer das Token verwenden kann. Es reicht dann der Besitz. BearerTokenAdditionalAuthTip: Wird hier nichts angegeben, werden keine Einschränkungen daran gesetzt, wer das Token verwenden kann. Es reicht dann der Besitz.
BearerTokenOverrideExpiration: Ablaufzeitpunkt überschreiben BearerTokenOverrideExpiration: Ablaufzeitpunkt überschreiben
@ -2564,4 +2579,7 @@ TestDownloadMaxSize: Maximale Dateigröße
TestDownloadMode: Modus TestDownloadMode: Modus
TestDownloadDirect: Direkte Generierung TestDownloadDirect: Direkte Generierung
TestDownloadInTransaction: Generierung während Datenbank-Transaktion TestDownloadInTransaction: Generierung während Datenbank-Transaktion
TestDownloadFromDatabase: Generierung während Download aus Datenbank TestDownloadFromDatabase: Generierung während Download aus Datenbank
ValueRequiredLabeledSimple fieldLabel@Text: #{fieldLabel} wird benötigt
ValueRequiredLabeledMultiWord fieldLabel@Text: „#{fieldLabel}“ wird benötigt

View File

@ -2563,3 +2563,6 @@ TestDownloadMode: Mode
TestDownloadDirect: Direct generation TestDownloadDirect: Direct generation
TestDownloadInTransaction: Generate during database transaction TestDownloadInTransaction: Generate during database transaction
TestDownloadFromDatabase: Generate while streaming from database TestDownloadFromDatabase: Generate while streaming from database
ValueRequiredLabeledSimple fieldLabel: #{fieldLabel} is required
ValueRequiredLabeledMultiWord fieldLabel: “#{fieldLabel}” is required

View File

@ -18,14 +18,15 @@ data DummyMessage = MsgDummyIdent
dummyForm :: ( RenderMessage (HandlerSite m) FormMessage dummyForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
, RenderMessage (HandlerSite m) DummyMessage , RenderMessage (HandlerSite m) DummyMessage
, YesodPersist (HandlerSite m) , YesodPersist (HandlerSite m)
, SqlBackendCanRead (YesodPersistBackend (HandlerSite m)) , SqlBackendCanRead (YesodPersistBackend (HandlerSite m))
, MonadHandler m , MonadHandler m
) => AForm m (CI Text) ) => WForm m (FormResult (CI Text))
dummyForm = wFormToAForm $ do dummyForm = do
mr <- getMessageRender mr <- getMessageRender
aFormToWForm $ areq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing wreq (ciField & addDatalist userList) (fslpI MsgDummyIdent (mr MsgDummyIdentPlaceholder) & noAutocomplete & addName PostLoginDummy) Nothing
where where
userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent]) userList = fmap mkOptionList . runDB $ withReaderT projectBackend (map toOption <$> selectList [] [Asc UserIdent] :: ReaderT SqlBackend _ [Option UserIdent])
toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent) toOption (Entity _ User{..}) = Option userDisplayName userIdent (CI.original userIdent)
@ -36,6 +37,7 @@ dummyLogin :: forall site.
, SqlBackendCanRead (YesodPersistBackend site) , SqlBackendCanRead (YesodPersistBackend site)
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, RenderMessage site DummyMessage , RenderMessage site DummyMessage
, RenderMessage site (ValueRequired site)
, Button site ButtonSubmit , Button site ButtonSubmit
) => AuthPlugin site ) => AuthPlugin site
dummyLogin = AuthPlugin{..} dummyLogin = AuthPlugin{..}
@ -44,8 +46,8 @@ dummyLogin = AuthPlugin{..}
apName = "dummy" apName = "dummy"
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch "POST" [] = liftSubHandler $ do apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderAForm FormStandard dummyForm ((loginRes, _), _) <- runFormPost $ renderWForm FormStandard dummyForm
tp <- getRouteToParent tp <- getRouteToParent
case loginRes of case loginRes of
FormFailure errs -> do FormFailure errs -> do
@ -55,15 +57,16 @@ dummyLogin = AuthPlugin{..}
addMessageI Warning MsgDummyNoFormData addMessageI Warning MsgDummyNoFormData
redirect $ tp LoginR redirect $ tp LoginR
FormSuccess ident -> FormSuccess ident ->
setCredsRedirect $ Creds "dummy" (CI.original ident) [] setCredsRedirect $ Creds apName (CI.original ident) []
apDispatch _ [] = badMethod
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site () apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard dummyForm
let loginForm = wrapForm login FormSettings let loginForm = wrapForm login FormSettings
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR "dummy" [] , formAction = Just . SomeRoute . toMaster $ PluginR apName []
, formEncoding = loginEnctype , formEncoding = loginEnctype
, formAttrs = [("uw-no-navigate-away-prompt","")] , formAttrs = [("uw-no-navigate-away-prompt","")]
, formSubmit = FormSubmit , formSubmit = FormSubmit

View File

@ -34,8 +34,6 @@ data CampusMessage = MsgCampusIdentPlaceholder
| MsgCampusIdent | MsgCampusIdent
| MsgCampusPassword | MsgCampusPassword
| MsgCampusPasswordPlaceholder | MsgCampusPasswordPlaceholder
| MsgCampusSubmit
| MsgCampusInvalidCredentials
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
@ -152,18 +150,15 @@ campusUserMatr' pool mode
campusForm :: ( RenderMessage (HandlerSite m) FormMessage campusForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
, RenderMessage (HandlerSite m) CampusMessage , RenderMessage (HandlerSite m) CampusMessage
, MonadHandler m , MonadHandler m
) => WForm m (FormResult CampusLogin) ) => WForm m (FormResult CampusLogin)
campusForm = do campusForm = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
aFormToWForm $ CampusLogin
ident <- wreq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing <$> areq ciField (fslpI MsgCampusIdent (mr MsgCampusIdentPlaceholder) & addAttr "autofocus" "") Nothing
password <- wreq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing <*> areq passwordField (fslpI MsgCampusPassword (mr MsgCampusPasswordPlaceholder)) Nothing
return $ CampusLogin
<$> ident
<*> password
apLdap :: Text apLdap :: Text
apLdap = "LDAP" apLdap = "LDAP"
@ -172,6 +167,7 @@ campusLogin :: forall site.
( YesodAuth site ( YesodAuth site
, RenderMessage site CampusMessage , RenderMessage site CampusMessage
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, RenderMessage site (ValueRequired site)
, Button site ButtonSubmit , Button site ButtonSubmit
) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site ) => Failover (LdapConf, LdapPool) -> FailoverMode -> AuthPlugin site
campusLogin pool mode = AuthPlugin{..} campusLogin pool mode = AuthPlugin{..}
@ -180,38 +176,37 @@ campusLogin pool mode = AuthPlugin{..}
apName = apLdap apName = apLdap
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch "POST" [] = liftSubHandler $ do apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm ((loginRes, _), _) <- runFormPost $ renderWForm FormStandard campusForm
tp <- getRouteToParent tp <- getRouteToParent
case loginRes of
FormFailure errs -> do resp <- formResultMaybe loginRes $ \CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> Just <$> do
forM_ errs $ addMessage Error . toHtml ldapResult <- withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do
redirect $ tp LoginR Ldap.bind ldap ldapDn ldapPassword
FormMissing -> redirect $ tp LoginR searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName]
FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do case searchResults of
ldapResult <- withLdapFailover _2 pool mode $ \(conf@LdapConf{..}, ldap) -> liftIO $ do [Ldap.SearchEntry (Ldap.Dn userDN) userAttrs]
Ldap.bind ldap ldapDn ldapPassword | [principalName] <- fold [ v | (k, v) <- userAttrs, k == ldapUserPrincipalName ]
searchResults <- findUser conf ldap campusIdent [ldapUserPrincipalName] , Right credsIdent <- Text.decodeUtf8' principalName
case searchResults of -> Right (userDN, credsIdent) <$ Ldap.bind ldap (Ldap.Dn credsIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword)
[Ldap.SearchEntry (Ldap.Dn userDN) userAttrs] other -> return $ Left other
| [principalName] <- fold [ v | (k, v) <- userAttrs, k == ldapUserPrincipalName ] case ldapResult of
, Right credsIdent <- Text.decodeUtf8' principalName Left err
-> Right (userDN, credsIdent) <$ Ldap.bind ldap (Ldap.Dn credsIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword) | LdapError (Ldap.ResponseError (Ldap.ResponseErrorCode _ Ldap.InvalidCredentials _ _)) <- err
other -> return $ Left other -> do
case ldapResult of $logDebugS "LDAP" "Invalid credentials"
Left err loginErrorMessageI LoginR Msg.InvalidLogin
| LdapError (Ldap.ResponseError (Ldap.ResponseErrorCode _ Ldap.InvalidCredentials _ _)) <- err | otherwise -> do
-> do $logErrorS "LDAP" $ "Error during login: " <> tshow err
$logDebugS "LDAP" "Invalid credentials" loginErrorMessageI LoginR Msg.AuthError
loginErrorMessageI LoginR Msg.InvalidLogin Right (Right (userDN, credsIdent)) ->
| otherwise -> do setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
$logErrorS "LDAP" $ "Error during login: " <> tshow err Right (Left searchResults) -> do
loginErrorMessageI LoginR Msg.AuthError $logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults
Right (Right (userDN, credsIdent)) -> loginErrorMessageI LoginR Msg.AuthError
setCredsRedirect $ Creds apName credsIdent [("DN", userDN)]
Right (Left searchResults) -> do maybe (redirect $ tp LoginR) return resp
$logWarnS "LDAP" $ "Could not extract principal name: " <> tshow searchResults apDispatch _ [] = badMethod
loginErrorMessageI LoginR Msg.AuthError
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site () apLogin :: (Route Auth -> Route site) -> WidgetFor site ()

View File

@ -28,11 +28,12 @@ data PWHashMessage = MsgPWHashIdent
hashForm :: ( RenderMessage (HandlerSite m) FormMessage hashForm :: ( RenderMessage (HandlerSite m) FormMessage
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
, RenderMessage (HandlerSite m) PWHashMessage , RenderMessage (HandlerSite m) PWHashMessage
, MonadHandler m , MonadHandler m
) => AForm m HashLogin ) => WForm m (FormResult HashLogin)
hashForm = wFormToAForm $ do hashForm = do
mr <- getMessageRender MsgRenderer mr <- getMsgRenderer
aFormToWForm $ HashLogin aFormToWForm $ HashLogin
<$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing <$> areq ciField (fslpI MsgPWHashIdent (mr MsgPWHashIdentPlaceholder)) Nothing
<*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing <*> areq passwordField (fslpI MsgPWHashPassword (mr MsgPWHashPasswordPlaceholder)) Nothing
@ -45,6 +46,7 @@ hashLogin :: forall site.
, PersistRecordBackend User (YesodPersistBackend site) , PersistRecordBackend User (YesodPersistBackend site)
, RenderMessage site PWHashMessage , RenderMessage site PWHashMessage
, RenderMessage site AFormMessage , RenderMessage site AFormMessage
, RenderMessage site (ValueRequired site)
, Button site ButtonSubmit , Button site ButtonSubmit
) => PWHashAlgorithm -> AuthPlugin site ) => PWHashAlgorithm -> AuthPlugin site
hashLogin pwHashAlgo = AuthPlugin{..} hashLogin pwHashAlgo = AuthPlugin{..}
@ -53,31 +55,30 @@ hashLogin pwHashAlgo = AuthPlugin{..}
apName = "PWHash" apName = "PWHash"
apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent apDispatch :: forall m. MonadAuthHandler site m => Text -> [Text] -> m TypedContent
apDispatch "POST" [] = liftSubHandler $ do apDispatch method [] | encodeUtf8 method == methodPost = liftSubHandler $ do
((loginRes, _), _) <- runFormPost $ renderAForm FormStandard hashForm ((loginRes, _), _) <- runFormPost $ renderWForm FormStandard hashForm
tp <- getRouteToParent tp <- getRouteToParent
case loginRes of
FormFailure errs -> do resp <- formResultMaybe loginRes $ \HashLogin{..} -> Just <$> do
forM_ errs $ addMessage Error . toHtml user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent
redirect $ tp LoginR case user of
FormMissing -> redirect $ tp LoginR Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent })
FormSuccess HashLogin{..} -> do | verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic.
user <- liftHandler . runDB . getBy $ UniqueAuthentication hashIdent setCredsRedirect $ Creds apName userIdent []
case user of other -> do
Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent }) $logDebugS "PWHash" $ tshow other
| verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic. loginErrorMessageI LoginR Msg.InvalidLogin
setCredsRedirect $ Creds apName userIdent []
other -> do maybe (redirect $ tp LoginR) return resp
$logDebugS "PWHash" $ tshow other apDispatch _ [] = badMethod
loginErrorMessageI LoginR Msg.InvalidLogin
apDispatch _ _ = notFound apDispatch _ _ = notFound
apLogin :: (Route Auth -> Route site) -> WidgetFor site () apLogin :: (Route Auth -> Route site) -> WidgetFor site ()
apLogin toMaster = do apLogin toMaster = do
(login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderWForm FormStandard hashForm
let loginForm = wrapForm login FormSettings let loginForm = wrapForm login FormSettings
{ formMethod = POST { formMethod = POST
, formAction = Just . SomeRoute . toMaster $ PluginR "PWHash" [] , formAction = Just . SomeRoute . toMaster $ PluginR apName []
, formEncoding = loginEnctype , formEncoding = loginEnctype
, formAttrs = [("uw-no-navigate-away-prompt","")] , formAttrs = [("uw-no-navigate-away-prompt","")]
, formSubmit = FormSubmit , formSubmit = FormSubmit

View File

@ -81,8 +81,6 @@ import Utils.Metrics
import Text.Cassius (cassiusFile) import Text.Cassius (cassiusFile)
import Yesod.Form.I18n.German
import Yesod.Form.I18n.English
import qualified Yesod.Auth.Message as Auth import qualified Yesod.Auth.Message as Auth
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
@ -239,15 +237,7 @@ appLanguagesOpts = do
} }
langOptions = map mkOption $ toList appLanguages langOptions = map mkOption $ toList appLanguages
return $ mkOptionList langOptions return $ mkOptionList langOptions
-- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages.
instance RenderMessage UniWorX FormMessage where
renderMessage _ ls = case lang of
("en" : _) -> englishFormMessage
_other -> germanFormMessage
where lang = Text.splitOn "-" $ selectLanguage' appLanguages ls
instance RenderMessage UniWorX WeekDay where instance RenderMessage UniWorX WeekDay where
renderMessage _ ls wDay = pack . fst $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7) renderMessage _ ls wDay = pack . fst $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7)
@ -256,6 +246,7 @@ newtype ShortWeekDay = ShortWeekDay { longWeekDay :: WeekDay }
instance RenderMessage UniWorX ShortWeekDay where instance RenderMessage UniWorX ShortWeekDay where
renderMessage _ ls (ShortWeekDay wDay) = pack . snd $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7) renderMessage _ ls (ShortWeekDay wDay) = pack . snd $ wDays (getTimeLocale' ls) !! (fromEnum wDay `mod` 7)
-- Access Control -- Access Control
newtype InvalidAuthTag = InvalidAuthTag Text newtype InvalidAuthTag = InvalidAuthTag Text
deriving (Eq, Ord, Show, Read, Generic, Typeable) deriving (Eq, Ord, Show, Read, Generic, Typeable)

View File

@ -34,6 +34,9 @@ import Utils.Form
import GHC.Exts (IsList(..)) import GHC.Exts (IsList(..))
import Yesod.Form.I18n.German
import Yesod.Form.I18n.English
appLanguages :: NonEmpty Lang appLanguages :: NonEmpty Lang
appLanguages = "de-de-formal" :| ["en-eu"] appLanguages = "de-de-formal" :| ["en-eu"]
@ -324,3 +327,26 @@ instance RenderMessage UniWorX UniWorXMessages where
uniworxMessages :: [UniWorXMessage] -> UniWorXMessages uniworxMessages :: [UniWorXMessage] -> UniWorXMessages
uniworxMessages = UniWorXMessages . map SomeMessage uniworxMessages = UniWorXMessages . map SomeMessage
-- This instance is required to use forms. You can modify renderMessage to
-- achieve customized and internationalized form validation messages.
instance RenderMessage UniWorX FormMessage where
renderMessage _ ls | lang == "en" = englishFormMessage
| otherwise = germanFormMessage
where lang = selectLanguage' ("de" :| ["en"]) ls
instance RenderMessage UniWorX (ValueRequired UniWorX) where
renderMessage foundation ls (ValueRequired label')
| [w] <- Text.words label
, let w' = Text.strip w
, not $ Text.null w'
= mr $ MsgValueRequiredLabeledSimple w'
| Text.null $ Text.strip label
= mr MsgValueRequired
| otherwise
= mr . MsgValueRequiredLabeledMultiWord $ Text.strip label
where
label = mr label'
mr :: forall msg. RenderMessage UniWorX msg => msg -> Text
mr = renderMessage foundation ls

View File

@ -250,7 +250,7 @@ postAdminFeaturesR = do
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
textInputCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) textInputCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
(\row _mkUnique -> bimap (fmap $ set lensRes . assertM (not . Text.null)) fvInput (\row _mkUnique -> bimap (fmap $ set lensRes . assertM (not . Text.null)) fvWidget
<$> mopt (textField & cfStrip) "" (Just $ row ^. lensDefault) <$> mopt (textField & cfStrip) "" (Just $ row ^. lensDefault)
) )
@ -261,7 +261,7 @@ postAdminFeaturesR = do
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
checkboxCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) checkboxCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
( \row _mkUnique -> bimap (fmap $ set lensRes) fvInput ( \row _mkUnique -> bimap (fmap $ set lensRes) fvWidget
<$> mpopt checkBoxField "" (Just $ row ^. lensDefault) <$> mpopt checkBoxField "" (Just $ row ^. lensDefault)
) )
@ -272,7 +272,7 @@ postAdminFeaturesR = do
-- -> DBRow r -- -> DBRow r
-- -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -- -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
-- termKeyCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) -- termKeyCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
-- ( \row _mkUnique -> (\(res, fieldView) -> (set lensRes <$> res, fvInput fieldView)) -- ( \row _mkUnique -> (\(res, fieldView) -> (set lensRes <$> res, fvWidget fieldView))
-- <$> mopt (intField & isoField (from _StudyTermsId)) "" (Just $ row ^. lensDefault) -- <$> mopt (intField & isoField (from _StudyTermsId)) "" (Just $ row ^. lensDefault)
-- ) -- )
@ -283,10 +283,11 @@ postAdminFeaturesR = do
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
parentsCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) parentsCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
( \row mkUnique -> bimap (fmap $ set lensRes . Set.fromList) fvInput ( \row mkUnique -> bimap (fmap $ set lensRes . Set.fromList) fvWidget
<$> massInputList <$> massInputList
(intField & isoField (from _StudyTermsId)) (intField & isoField (from _StudyTermsId))
(const "") (const "")
MsgStudyTermsParentMissing
(Just . SomeRoute . (AdminFeaturesR :#:)) (Just . SomeRoute . (AdminFeaturesR :#:))
(mkUnique ("parents" :: Text)) (mkUnique ("parents" :: Text))
"" ""
@ -302,7 +303,7 @@ postAdminFeaturesR = do
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
degreeCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) degreeCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
( \row _mkUnique -> bimap (fmap $ set lensRes) fvInput ( \row _mkUnique -> bimap (fmap $ set lensRes) fvWidget
<$> mopt degreeField "" (Just $ row ^. lensDefault) <$> mopt degreeField "" (Just $ row ^. lensDefault)
) )
@ -313,7 +314,7 @@ postAdminFeaturesR = do
-> DBRow r -> DBRow r
-> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r))) -> DBCell (MForm (HandlerFor UniWorX)) (FormResult (DBFormResult i a (DBRow r)))
fieldTypeCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex) fieldTypeCell lensRes lensDefault lensIndex = formCell id (return . view lensIndex)
( \row _mkUnique -> bimap (fmap $ set lensRes) fvInput ( \row _mkUnique -> bimap (fmap $ set lensRes) fvWidget
<$> mopt (selectField optionsFinite) "" (Just $ row ^. lensDefault) <$> mopt (selectField optionsFinite) "" (Just $ row ^. lensDefault)
) )

View File

@ -157,7 +157,7 @@ postAdminTestR = do
(addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing -- Any old field; for demonstration (addRes, addView) <- mpreq textField ("" & addName (nudge "text")) Nothing -- Any old field; for demonstration
let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes -- Do something semi-interesting on the result of the @textField@ to demonstrate that further processing can be done let addRes' = fromMaybe 0 . readMay . Text.filter isDigit <$> addRes -- Do something semi-interesting on the result of the @textField@ to demonstrate that further processing can be done
addRes'' = addRes' <&> \dat prev -> FormSuccess (Map.singleton (maybe 0 (succ . fst) $ Map.lookupMax prev) dat) -- Construct the callback to determine new cell positions and data within @FormResult@ as required, nested @FormResult@ allows aborting the add depending on previous data addRes'' = addRes' <&> \dat prev -> FormSuccess (Map.singleton (maybe 0 (succ . fst) $ Map.lookupMax prev) dat) -- Construct the callback to determine new cell positions and data within @FormResult@ as required, nested @FormResult@ allows aborting the add depending on previous data
return (addRes'', toWidget csrf >> fvInput addView >> fvInput submitBtn) return (addRes'', toWidget csrf >> fvWidget addView >> fvWidget submitBtn)
mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form" mkAddForm _pos _dim _ _ = error "Dimension and Position is always 0 for our 1-dimensional form"
-- | Make a single massInput-Cell -- | Make a single massInput-Cell
@ -170,7 +170,7 @@ postAdminTestR = do
-> Form Int -> Form Int
mkCellForm _pos cData initial nudge csrf = do -- Extremely simple cell mkCellForm _pos cData initial nudge csrf = do -- Extremely simple cell
(intRes, intView) <- mreq intField ("" & addName (nudge "int")) $ initial <|> Just cData (intRes, intView) <- mreq intField ("" & addName (nudge "int")) $ initial <|> Just cData
return (intRes, toWidget csrf >> fvInput intView) return (intRes, toWidget csrf >> fvWidget intView)
-- | How does the shape (`ListLength`) change if a certain cell is deleted? -- | How does the shape (`ListLength`) change if a certain cell is deleted?
deleteCell :: Map ListPosition Int -- ^ Current shape, including initialisation data deleteCell :: Map ListPosition Int -- ^ Current shape, including initialisation data
-> ListPosition -- ^ Coordinate to delete -> ListPosition -- ^ Coordinate to delete
@ -185,7 +185,7 @@ postAdminTestR = do
-- The actual call to @massInput@ is comparatively simple: -- The actual call to @massInput@ is comparatively simple:
((miResult, fvInput -> miForm), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd (\_ _ _ -> Set.empty) buttonAction defaultMiLayout ("massinput" :: Text)) "" True Nothing ((miResult, fvWidget -> miForm), miEnc) <- runFormPost . identifyForm ("massinput" :: Text) $ massInput (MassInput mkAddForm mkCellForm deleteCell allowAdd (\_ _ _ -> Set.empty) buttonAction defaultMiLayout ("massinput" :: Text)) "" True Nothing
testDownloadWidget <- testDownload testDownloadWidget <- testDownload

View File

@ -31,13 +31,13 @@ bearerTokenForm = do
muid <- maybeAuthId muid <- maybeAuthId
mr <- getMessageRender mr <- getMessageRender
btfAuthorityGroups <- aFormToWForm $ HashSet.fromList . map Left <$> massInputListA pathPieceField (const "") (\p -> Just . SomeRoute $ AdminTokensR :#: p) ("token-groups" :: Text) (fslI MsgBearerTokenAuthorityGroups & setTooltip MsgBearerTokenAuthorityGroupsTip) False Nothing btfAuthorityGroups <- aFormToWForm $ HashSet.fromList . map Left <$> massInputListA pathPieceField (const "") MsgBearerTokenAuthorityGroupMissing (\p -> Just . SomeRoute $ AdminTokensR :#: p) ("token-groups" :: Text) (fslI MsgBearerTokenAuthorityGroups & setTooltip MsgBearerTokenAuthorityGroupsTip) False Nothing
btfAuthorityUsers <- fmap (fmap . ofoldMap $ HashSet.singleton . Right) <$> wopt (checkMap (foldMapM $ fmap Set.singleton . left MsgBearerTokenAuthorityUnknownUser) (Set.map Right) $ multiUserField False Nothing) (fslpI MsgBearerTokenAuthorityUsers (mr MsgLdapIdentificationOrEmail) & setTooltip MsgBearerTokenAuthorityUsersTip) (Just $ Set.singleton <$> muid) btfAuthorityUsers <- fmap (fmap . ofoldMap $ HashSet.singleton . Right) <$> wopt (checkMap (foldMapM $ fmap Set.singleton . left MsgBearerTokenAuthorityUnknownUser) (Set.map Right) $ multiUserField False Nothing) (fslpI MsgBearerTokenAuthorityUsers (mr MsgLdapIdentificationOrEmail) & setTooltip MsgBearerTokenAuthorityUsersTip) (Just $ Set.singleton <$> muid)
let btfAuthority' :: FormResult (HashSet (Either UserGroupName UserId)) let btfAuthority' :: FormResult (HashSet (Either UserGroupName UserId))
btfAuthority' btfAuthority'
= (<>) <$> btfAuthorityGroups <*> ((hoistMaybe =<< btfAuthorityUsers) <|> pure HashSet.empty) = (<>) <$> btfAuthorityGroups <*> ((hoistMaybe =<< btfAuthorityUsers) <|> pure HashSet.empty)
let btfRoutesForm = HashSet.fromList <$> massInputListA routeField (const "") (\p -> Just . SomeRoute $ AdminTokensR :#: p) ("token-routes" :: Text) (fslI MsgBearerTokenRoutes & setTooltip MsgBearerTokenRoutesTip) True Nothing let btfRoutesForm = HashSet.fromList <$> massInputListA routeField (const "") MsgBearerTokenRouteMissing (\p -> Just . SomeRoute $ AdminTokensR :#: p) ("token-routes" :: Text) (fslI MsgBearerTokenRoutes & setTooltip MsgBearerTokenRoutesTip) True Nothing
btfRoutes' <- optionalActionW btfRoutesForm (fslI MsgBearerTokenRestrictRoutes) (Just True) btfRoutes' <- optionalActionW btfRoutesForm (fslI MsgBearerTokenRestrictRoutes) (Just True)
let btfRestrictForm = massInputAccumEditW miAdd' miCell' (\p -> Just . SomeRoute $ AdminTokensR :#: p) miLayout' ("token-restrictions" :: Text) (fslI MsgBearerTokenRestrictions) False Nothing let btfRestrictForm = massInputAccumEditW miAdd' miCell' (\p -> Just . SomeRoute $ AdminTokensR :#: p) miLayout' ("token-restrictions" :: Text) (fslI MsgBearerTokenRestrictions) False Nothing
@ -48,8 +48,8 @@ bearerTokenForm = do
-> Either (FieldView UniWorX) (Route UniWorX, Value) -> Either (FieldView UniWorX) (Route UniWorX, Value)
-> Form (Route UniWorX, Value) -> Form (Route UniWorX, Value)
miForm nudge mode csrf = do miForm nudge mode csrf = do
(routeRes, routeView) <- mpreq routeField ("" & addName (nudge "route")) (mode ^? _Right . _1) (routeRes, routeView) <- mpreq routeField (fslI MsgBearerTokenRestrictRoute & addName (nudge "route")) (mode ^? _Right . _1)
(restrRes, restrView) <- mpreq (checkMap (left Text.pack . Aeson.eitherDecode . encodeUtf8 . fromStrict . unTextarea) (Textarea . toStrict . decodeUtf8 . Aeson.encodePretty) textareaField) ("" & addName (nudge "restr")) (mode ^? _Right . _2) (restrRes, restrView) <- mpreq (checkMap (left Text.pack . Aeson.eitherDecode . encodeUtf8 . fromStrict . unTextarea) (Textarea . toStrict . decodeUtf8 . Aeson.encodePretty) textareaField) (fslI MsgBearerTokenRestrictValue & addName (nudge "restr")) (mode ^? _Right . _2)
return ((,) <$> routeRes <*> restrRes, case mode of return ((,) <$> routeRes <*> restrRes, case mode of
Left btn -> $(widgetFile "widgets/massinput/token-restrictions/add") Left btn -> $(widgetFile "widgets/massinput/token-restrictions/add")

View File

@ -217,14 +217,14 @@ colPseudonyms = sortable Nothing (i18nCell MsgPseudonyms) $ \DBRow{ dbrOutput=(_
colRatedField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (Bool, a, b) CorrectionTableData))) colRatedField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (Bool, a, b) CorrectionTableData)))
colRatedField = sortable Nothing (i18nCell MsgRatingDone) $ formCell id colRatedField = sortable Nothing (i18nCell MsgRatingDone) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ (submissionRatingDone -> done), _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) (_1 .~) . over _2 fvInput <$> mreq checkBoxField (fsUniq mkUnique "rated") (Just done)) (\DBRow{ dbrOutput=(Entity _ (submissionRatingDone -> done), _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) (_1 .~) . over _2 fvWidget <$> mreq checkBoxField (fsUniq mkUnique "rated") (Just done))
colPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData))) colPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData)))
colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id colPointsField = sortable (Just "rating") (i18nCell MsgColumnRatingPoints) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _, _, _, _) } mkUnique -> case sheetType of (\DBRow{ dbrOutput=(Entity _ Submission{..}, Entity _ Sheet{..}, _, _, _, _, _, _) } mkUnique -> case sheetType of
NotGraded -> over (_1.mapped) (_2 .~) <$> pure (FormSuccess Nothing, mempty) NotGraded -> over (_1.mapped) (_2 .~) <$> pure (FormSuccess Nothing, mempty)
_other -> over (_1.mapped) (_2 .~) . over _2 fvInput <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints) _other -> over (_1.mapped) (_2 .~) . over _2 fvWidget <$> mopt (pointsFieldMax $ preview (_grading . _maxPoints) sheetType) (fsUniq mkUnique "points") (Just submissionRatingPoints)
) )
colMaxPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData))) colMaxPointsField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, Maybe Points, b) CorrectionTableData)))
@ -233,7 +233,7 @@ colMaxPointsField = sortable (Just "sheet-type") (i18nCell MsgSheetType) $ i18nC
colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData))) colCommentField :: Colonnade Sortable CorrectionTableData (DBCell _ (FormResult (DBFormResult SubmissionId (a, b, Maybe Text) CorrectionTableData)))
colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id colCommentField = sortable (Just "comment") (i18nCell MsgRatingComment) $ fmap (cellAttrs <>~ [("style","width:60%")]) $ formCell id
(\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId) (\DBRow{ dbrOutput=(Entity subId _, _, _, _, _, _, _, _) } -> return subId)
(\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvInput <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment)) (\DBRow{ dbrOutput=(Entity _ Submission{..}, _, _, _, _, _, _, _) } mkUnique -> over (_1.mapped) ((_3 .~) . assertM (not . null) . fmap (Text.strip . unTextarea)) . over _2 fvWidget <$> mopt textareaField (fsUniq mkUnique "comment") (Just $ Textarea <$> submissionRatingComment))
colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a) colLastEdit :: IsDBTable m a => Colonnade Sortable CorrectionTableData (DBCell m a)
colLastEdit = sortable (Just "last-edit") (i18nCell MsgLastEdit) $ colLastEdit = sortable (Just "last-edit") (i18nCell MsgLastEdit) $

View File

@ -128,7 +128,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
let miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId)))) let miAdd :: ListPosition -> Natural -> (Text -> Text) -> FieldView UniWorX -> Maybe (Form (Map ListPosition (Either UserEmail UserId) -> FormResult (Map ListPosition (Either UserEmail UserId))))
miAdd _ _ nudge btn = Just $ \csrf -> do miAdd _ _ nudge btn = Just $ \csrf -> do
(addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) ("" & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing (addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgCourseLecturerEmail & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
let addRes'' = addRes <&> \newDat oldDat -> if let addRes'' = addRes <&> \newDat oldDat -> if
| existing <- newDat `Set.intersection` Set.fromList (Map.elems oldDat) | existing <- newDat `Set.intersection` Set.fromList (Map.elems oldDat)
, not $ Set.null existing , not $ Set.null existing
@ -140,7 +140,7 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse . validateFormDB
miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType) miCell :: ListPosition -> Either UserEmail UserId -> Maybe (Maybe LecturerType) -> (Text -> Text) -> Form (Maybe LecturerType)
miCell _ (Right lid) defType nudge = \csrf -> do miCell _ (Right lid) defType nudge = \csrf -> do
(lrwRes,lrwView) <- mreq (selectField optionsFinite) ("" & addName (nudge "lecturer-type")) (join defType) (lrwRes,lrwView) <- mreq (selectField optionsFinite) (fslI MsgLecturerType & addName (nudge "lecturer-type")) (join defType)
User{userEmail, userDisplayName, userSurname} <- liftHandler . runDB $ get404 lid User{userEmail, userDisplayName, userSurname} <- liftHandler . runDB $ get404 lid
let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown") let lrwView' = $(widgetFile "course/lecturerMassInput/cellKnown")
return (Just <$> lrwRes,lrwView') return (Just <$> lrwRes,lrwView')

View File

@ -103,7 +103,7 @@ courseUserProfileSection (Entity cid Course{..}) (Entity uid User{ userShowSex =
((regFieldRes, regFieldView), regFieldEnctype) <- lift . runFormPost . identifyForm FIDcRegField $ \csrf -> ((regFieldRes, regFieldView), regFieldEnctype) <- lift . runFormPost . identifyForm FIDcRegField $ \csrf ->
let currentField :: Maybe (Maybe StudyFeaturesId) let currentField :: Maybe (Maybe StudyFeaturesId)
currentField = courseParticipantField . entityVal <$> mRegistration currentField = courseParticipantField . entityVal <$> mRegistration
in over _2 ((toWidget csrf <>) . fvInput) <$> mreq (studyFeaturesFieldFor Nothing True (maybeToList $ join currentField) $ Just uid) ("" & addAutosubmit) currentField in over _2 ((toWidget csrf <>) . fvWidget) <$> mreq (studyFeaturesFieldFor Nothing True (maybeToList $ join currentField) $ Just uid) ("" & addAutosubmit) currentField
let registrationFieldFrag :: Text let registrationFieldFrag :: Text
registrationFieldFrag = "registration-field" registrationFieldFrag = "registration-field"

View File

@ -77,7 +77,7 @@ examAutoOccurrenceAcceptForm :: Maybe ExamAutoOccurrenceAcceptForm -> Form ExamA
examAutoOccurrenceAcceptForm confirmData = identifyForm FIDExamAutoOccurrenceConfirm $ \html -> do examAutoOccurrenceAcceptForm confirmData = identifyForm FIDExamAutoOccurrenceConfirm $ \html -> do
(confirmDataRes, confirmDataView) <- mreq secretJsonField "" confirmData (confirmDataRes, confirmDataView) <- mreq secretJsonField "" confirmData
(acceptRes, acceptView) <- buttonForm' [BtnExamAutoOccurrenceAccept] mempty (acceptRes, acceptView) <- buttonForm' [BtnExamAutoOccurrenceAccept] mempty
return (acceptRes *> confirmDataRes, toWidget html <> fvInput confirmDataView <> acceptView) return (acceptRes *> confirmDataRes, toWidget html <> fvWidget confirmDataView <> acceptView)
examAutoOccurrenceCalculateWidget :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Widget examAutoOccurrenceCalculateWidget :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Widget

View File

@ -114,7 +114,7 @@ examCorrectorsForm mPrev = wFormToAForm $ do
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
miAdd' nudge submitView csrf = do miAdd' nudge submitView csrf = do
(addRes, addView) <- mpreq (multiUserInvitationField . MUILookupAnyUser $ Just corrUserSuggestions) ("" & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing (addRes, addView) <- mpreq (multiUserInvitationField . MUILookupAnyUser $ Just corrUserSuggestions) (fslI MsgExamCorrectorEmail & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
let let
addRes' addRes'
| otherwise | otherwise
@ -160,12 +160,12 @@ examOccurrenceForm prev = wFormToAForm $ do
where where
examOccurrenceForm' nudge mPrev csrf = do examOccurrenceForm' nudge mPrev csrf = do
(eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev) (eofIdRes, eofIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ eofId =<< mPrev)
(eofNameRes, eofNameView) <- mpreq (textField & cfStrip & cfCI) ("" & addName (nudge "name")) (eofName <$> mPrev) (eofNameRes, eofNameView) <- mpreq (textField & cfStrip & cfCI) (fslI MsgExamRoomName & addName (nudge "name")) (eofName <$> mPrev)
(eofRoomRes, eofRoomView) <- mpreq (textField & cfStrip) ("" & addName (nudge "room")) (eofRoom <$> mPrev) (eofRoomRes, eofRoomView) <- mpreq (textField & cfStrip) (fslI MsgExamRoomRoom & addName (nudge "room")) (eofRoom <$> mPrev)
(eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) ("" & addName (nudge "capacity")) (eofCapacity <$> mPrev) (eofCapacityRes, eofCapacityView) <- mpreq (natFieldI MsgExamRoomCapacityNegative) (fslI MsgExamRoomCapacity & addName (nudge "capacity")) (eofCapacity <$> mPrev)
(eofStartRes, eofStartView) <- mpreq utcTimeField ("" & addName (nudge "start")) (eofStart <$> mPrev) (eofStartRes, eofStartView) <- mpreq utcTimeField (fslI MsgExamRoomStart & addName (nudge "start")) (eofStart <$> mPrev)
(eofEndRes, eofEndView) <- mopt utcTimeField ("" & addName (nudge "end")) (eofEnd <$> mPrev) (eofEndRes, eofEndView) <- mopt utcTimeField (fslI MsgExamRoomEnd & addName (nudge "end")) (eofEnd <$> mPrev)
(eofDescRes, eofDescView) <- mopt htmlFieldSmall ("" & addName (nudge "description")) (eofDescription <$> mPrev) (eofDescRes, eofDescView) <- mopt htmlFieldSmall (fslI MsgExamRoomDescription & addName (nudge "description")) (eofDescription <$> mPrev)
return ( ExamOccurrenceForm return ( ExamOccurrenceForm
<$> eofIdRes <$> eofIdRes
@ -201,11 +201,11 @@ examPartsForm prev = wFormToAForm $ do
fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts) False $ Set.toList <$> prev fmap (fmap Set.fromList) . massInputAccumEditW miAdd' miCell' miButtonAction' miLayout' miIdent' (fslI MsgExamParts) False $ Set.toList <$> prev
where where
examPartForm' nudge mPrev csrf = do examPartForm' nudge mPrev csrf = do
(epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev) (epfIdRes, epfIdView) <- mopt hiddenField ("" & addName (nudge "id")) (Just $ epfId =<< mPrev)
(epfNumberRes, epfNumberView) <- mpreq (isoField (from _ExamPartNumber) $ textField & cfStrip & cfCI) ("" & addName (nudge "number") & addPlaceholder "1, 6a, 3.1.4, ...") (epfNumber <$> mPrev) (epfNumberRes, epfNumberView) <- mpreq (isoField (from _ExamPartNumber) $ textField & cfStrip & cfCI) (fslI MsgExamPartNumber & addName (nudge "number") & addPlaceholder "1, 6a, 3.1.4, ...") (epfNumber <$> mPrev)
(epfNameRes, epfNameView) <- mopt (textField & cfStrip & cfCI) ("" & addName (nudge "name")) (epfName <$> mPrev) (epfNameRes, epfNameView) <- mopt (textField & cfStrip & cfCI) (fslI MsgExamPartName & addName (nudge "name")) (epfName <$> mPrev)
(epfMaxPointsRes, epfMaxPointsView) <- mopt pointsField ("" & addName (nudge "max-points")) (epfMaxPoints <$> mPrev) (epfMaxPointsRes, epfMaxPointsView) <- mopt pointsField (fslI MsgExamPartMaxPoints & addName (nudge "max-points")) (epfMaxPoints <$> mPrev)
(epfWeightRes, epfWeightView) <- mpreq (checkBool (>= 0) MsgExamPartWeightNegative rationalField) ("" & addName (nudge "weight")) (epfWeight <$> mPrev <|> Just 1) (epfWeightRes, epfWeightView) <- mpreq (checkBool (>= 0) MsgExamPartWeightNegative rationalField) (fslI MsgExamPartWeight & addName (nudge "weight")) (epfWeight <$> mPrev <|> Just 1)
return ( ExamPartForm return ( ExamPartForm
<$> epfIdRes <$> epfIdRes

View File

@ -95,7 +95,7 @@ makeExamOfficeUsersForm template = renderWForm FormStandard $ do
-> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
miAdd' nudge btn csrf = do miAdd' nudge btn csrf = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
(addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) ("" & addName (nudge "users") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing (addRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgExamOfficeUserEmail & addName (nudge "users") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
let let
res' :: FormResult ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) res' :: FormResult ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
res' = addRes <&> \newUsers oldUsers -> if res' = addRes <&> \newUsers oldUsers -> if

View File

@ -72,11 +72,12 @@ externalExamForm template = validateForm validateExternalExam $ \html -> do
miIdent = "external-exams-school" miIdent = "external-exams-school"
fSettings = fslI MsgExternalExamExamOfficeSchools & setTooltip MsgExternalExamExamOfficeSchoolsTip fSettings = fslI MsgExternalExamExamOfficeSchools & setTooltip MsgExternalExamExamOfficeSchoolsTip
fRequired = False fRequired = False
staffForm :: Route UniWorX -> Maybe [Either UserEmail UserId] -> AForm Handler [Either UserEmail UserId]
staffForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired staffForm cRoute = massInputAccumA miAdd miCell miButtonAction miLayout miIdent fSettings fRequired
where where
miAdd mkUnique submitView csrf = do miAdd mkUnique submitView csrf = do
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
(usersRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) ("" & addName (mkUnique "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing (usersRes, addView) <- mpreq (multiUserInvitationField $ MUILookupAnyUser Nothing) (fslI MsgExternalExamStaffEmail & addName (mkUnique "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
let let
usersRes' = usersRes <&> \newDat oldDat -> if usersRes' = usersRes <&> \newDat oldDat -> if
| existing <- newDat `Set.intersection` Set.fromList oldDat | existing <- newDat `Set.intersection` Set.fromList oldDat

View File

@ -68,7 +68,7 @@ mkSchoolForm :: Maybe SchoolId -> Maybe SchoolForm -> Form SchoolForm
mkSchoolForm mSsh template = renderAForm FormStandard $ SchoolForm mkSchoolForm mSsh template = renderAForm FormStandard $ SchoolForm
<$> maybe (\f fs -> areq f fs (sfShorthand <$> template)) (\ssh f fs -> aforced f fs (unSchoolKey ssh)) mSsh (textField & cfStrip & cfCI) (fslI MsgSchoolShort) <$> maybe (\f fs -> areq f fs (sfShorthand <$> template)) (\ssh f fs -> aforced f fs (unSchoolKey ssh)) mSsh (textField & cfStrip & cfCI) (fslI MsgSchoolShort)
<*> areq (textField & cfStrip & cfCI) (fslI MsgSchoolName) (sfName <$> template) <*> areq (textField & cfStrip & cfCI) (fslI MsgSchoolName) (sfName <$> template)
<*> (Set.fromList . mapMaybe (fmap CI.mk . assertM' (not . Text.null) . Text.strip . CI.original) <$> massInputListA (ciField & addDatalist ldapOrgs) (const "") (const Nothing) ("ldap-organisations" :: Text) (fslI MsgSchoolLdapOrganisations & setTooltip MsgSchoolLdapOrganisationsTip) False (Set.toList . sfOrgUnits <$> template)) <*> (Set.fromList . mapMaybe (fmap CI.mk . assertM' (not . Text.null) . Text.strip . CI.original) <$> massInputListA (ciField & addDatalist ldapOrgs) (const "") MsgSchoolLdapOrganisationMissing (const Nothing) ("ldap-organisations" :: Text) (fslI MsgSchoolLdapOrganisations & setTooltip MsgSchoolLdapOrganisationsTip) False (Set.toList . sfOrgUnits <$> template))
where where
ldapOrgs :: HandlerFor UniWorX (OptionList (CI Text)) ldapOrgs :: HandlerFor UniWorX (OptionList (CI Text))
ldapOrgs = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $ ldapOrgs = fmap (mkOptionList . map (\t -> Option (CI.original t) t (CI.original t)) . Set.toAscList) . runDB $

View File

@ -420,7 +420,7 @@ getSShowR tid ssh csh shn = do
Entity _ SheetPseudonym{sheetPseudonymPseudonym} <- MaybeT . runDB . getBy $ UniqueSheetPseudonymUser sid uid Entity _ SheetPseudonym{sheetPseudonymPseudonym} <- MaybeT . runDB . getBy $ UniqueSheetPseudonymUser sid uid
return $ review _PseudonymText sheetPseudonymPseudonym return $ review _PseudonymText sheetPseudonymPseudonym
(generateWidget, generateEnctype) <- generateFormPost $ \csrf -> (generateWidget, generateEnctype) <- generateFormPost $ \csrf ->
over _2 ((toWidget csrf <>) . fvInput) <$> mreq (buttonField BtnGenerate) "" Nothing over _2 ((toWidget csrf <>) . fvWidget) <$> mreq (buttonField BtnGenerate) "" Nothing
let generateForm = wrapForm generateWidget def let generateForm = wrapForm generateWidget def
{ formAction = Just . SomeRoute $ CSheetR tid ssh csh shn SPseudonymR { formAction = Just . SomeRoute $ CSheetR tid ssh csh shn SPseudonymR
, formEncoding = generateEnctype , formEncoding = generateEnctype
@ -760,9 +760,9 @@ correctorForm loads' = wFormToAForm $ do
-> (Text -> Text) -> (Text -> Text)
-> Form (CorrectorState, Load) -> Form (CorrectorState, Load)
miCell _ userIdent initRes nudge csrf = do miCell _ userIdent initRes nudge csrf = do
(stateRes, stateView) <- mreq (selectField optionsFinite) ("" & addName (nudge "state")) $ (fst <$> initRes) <|> Just CorrectorNormal (stateRes, stateView) <- mreq (selectField optionsFinite) (fslI MsgSheetCorrectorState & addName (nudge "state")) $ (fst <$> initRes) <|> Just CorrectorNormal
(byTutRes, byTutView) <- mreq checkBoxField ("" & addName (nudge "bytut")) $ (isJust . byTutorial . snd <$> initRes) <|> Just False (byTutRes, byTutView) <- mreq checkBoxField ("" & addName (nudge "bytut")) $ (isJust . byTutorial . snd <$> initRes) <|> Just False
(propRes, propView) <- mreq (checkBool (>= 0) MsgProportionNegative $ rationalField) ("" & addName (nudge "prop")) $ (byProportion . snd <$> initRes) <|> Just 0 (propRes, propView) <- mreq (checkBool (>= 0) MsgProportionNegative $ rationalField) (fslI MsgSheetCorrectorProportion & addName (nudge "prop")) $ (byProportion . snd <$> initRes) <|> Just 0
let let
res :: FormResult (CorrectorState, Load) res :: FormResult (CorrectorState, Load)
res = (,) <$> stateRes <*> (Load <$> tutRes' <*> propRes) res = (,) <$> stateRes <*> (Load <$> tutRes' <*> propRes)

View File

@ -243,6 +243,7 @@ newTermForm template = validateForm validateTerm $ \html -> do
holidayForm = massInputListA holidayForm = massInputListA
dayField dayField
(const $ "" & addPlaceholder (mr MsgTermHolidayPlaceholder)) (const $ "" & addPlaceholder (mr MsgTermHolidayPlaceholder))
MsgTermHolidayMissing
(const Nothing) (const Nothing)
("holidays" :: Text) ("holidays" :: Text)
(fslI MsgTermHolidays) (fslI MsgTermHolidays)

View File

@ -40,7 +40,7 @@ tutorialForm cid template html = do
where where
miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId]) miAdd' :: (Text -> Text) -> FieldView UniWorX -> Form ([Either UserEmail UserId] -> FormResult [Either UserEmail UserId])
miAdd' nudge submitView csrf = do miAdd' nudge submitView csrf = do
(addRes, addView) <- mpreq (multiUserInvitationField . MUILookupAnyUser . Just $ tutUserSuggestions uid) ("" & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing (addRes, addView) <- mpreq (multiUserInvitationField . MUILookupAnyUser . Just $ tutUserSuggestions uid) (fslI MsgTutorEmail & addName (nudge "email") & addPlaceholder (mr MsgLdapIdentificationOrEmail)) Nothing
let let
addRes' addRes'
| otherwise | otherwise

View File

@ -42,7 +42,7 @@ import qualified Data.HashSet as HashSet
hijackUserForm :: Form () hijackUserForm :: Form ()
hijackUserForm csrf = do hijackUserForm csrf = do
(btnResult, btnView) <- mopt (buttonField BtnHijack) "" Nothing (btnResult, btnView) <- mopt (buttonField BtnHijack) "" Nothing
return (btnResult >>= guard . is _Just, mconcat [toWidget csrf, fvInput btnView]) return (btnResult >>= guard . is _Just, mconcat [toWidget csrf, fvWidget btnView])
-- In case of refactoring, use this: -- In case of refactoring, use this:
-- instance HasEntity (DBRow (Entity User)) User where -- instance HasEntity (DBRow (Entity User)) User where

View File

@ -73,7 +73,7 @@ confirmForm' drRecords confirmString mmsg = addDeleteTargets . identifyForm FIDD
addDeleteTargets :: Form a -> Form a addDeleteTargets :: Form a -> Form a
addDeleteTargets form csrf = do addDeleteTargets form csrf = do
(_, fvTargets) <- mreq secretJsonField ("" & addName (toPathPiece PostDeleteTarget)) (Just drRecords) (_, fvTargets) <- mreq secretJsonField ("" & addName (toPathPiece PostDeleteTarget)) (Just drRecords)
over _2 (mappend $ fvInput fvTargets) <$> form csrf over _2 (mappend $ fvWidget fvTargets) <$> form csrf
postDeleteR :: ( DeleteCascade record SqlBackend ) postDeleteR :: ( DeleteCascade record SqlBackend )

View File

@ -635,9 +635,9 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
sFileForm :: (Text -> Text) -> Maybe UploadSpecificFile -> Form UploadSpecificFile sFileForm :: (Text -> Text) -> Maybe UploadSpecificFile -> Form UploadSpecificFile
sFileForm nudge mPrevUF csrf = do sFileForm nudge mPrevUF csrf = do
(labelRes, labelView) <- mpreq textField ("" & addName (nudge "label")) $ specificFileLabel <$> mPrevUF (labelRes, labelView) <- mpreq textField (fslI MsgUploadSpecificFileLabel & addName (nudge "label")) $ specificFileLabel <$> mPrevUF
(nameRes, nameView) <- mpreq textField ("" & addName (nudge "name")) $ specificFileName <$> mPrevUF (nameRes, nameView) <- mpreq textField (fslI MsgUploadSpecificFileName & addName (nudge "name")) $ specificFileName <$> mPrevUF
(reqRes, reqView) <- mpreq checkBoxField ("" & addName (nudge "required")) $ specificFileRequired <$> mPrevUF (reqRes, reqView) <- mpreq checkBoxField (fslI MsgUploadSpecificFileRequired & addName (nudge "required")) $ specificFileRequired <$> mPrevUF
return ( UploadSpecificFile <$> labelRes <*> nameRes <*> reqRes return ( UploadSpecificFile <$> labelRes <*> nameRes <*> reqRes
, $(widgetFile "widgets/massinput/uploadSpecificFiles/form") , $(widgetFile "widgets/massinput/uploadSpecificFiles/form")

View File

@ -20,7 +20,7 @@ import Utils.Form
import Handler.Utils.Form.MassInput.Liveliness import Handler.Utils.Form.MassInput.Liveliness
import Handler.Utils.Form.MassInput.TH import Handler.Utils.Form.MassInput.TH
import Text.Blaze (Markup) import Text.Blaze (Markup, toMarkup)
import qualified Data.Text as Text import qualified Data.Text as Text
@ -426,7 +426,9 @@ massInput MassInput{ miIdent = toPathPiece -> miIdent, ..} FieldSettings{..} fvR
fvLabel = toHtml $ mr fsLabel fvLabel = toHtml $ mr fsLabel
fvTooltip = toHtml . mr <$> fsTooltip fvTooltip = toHtml . mr <$> fsTooltip
fvInput = $(widgetFile "widgets/massinput/massinput") fvInput = $(widgetFile "widgets/massinput/massinput")
fvErrors = Nothing fvErrors = case result of
FormFailure errs | not $ null errs -> Just . mconcat . intersperse [shamlet|<br />|] $ map toMarkup errs
_other -> Nothing
in return (result, FieldView{..}) in return (result, FieldView{..})
defaultMiLayout :: forall liveliness cellData cellResult. defaultMiLayout :: forall liveliness cellData cellResult.
@ -455,24 +457,26 @@ listMiLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/mas
-- | Wrapper around `massInput` for the common case, that we just want an arbitrary list of single fields without any constraints -- | Wrapper around `massInput` for the common case, that we just want an arbitrary list of single fields without any constraints
massInputList :: forall handler cellResult ident. massInputList :: forall handler cellResult ident msg.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadThrow handler , MonadThrow handler
, PathPiece ident , PathPiece ident
, RenderMessage UniWorX msg
) )
=> Field handler cellResult => Field handler cellResult
-> (ListPosition -> FieldSettings UniWorX) -> (ListPosition -> FieldSettings UniWorX)
-> msg
-> (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX))
-> ident -> ident
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Bool -> Bool
-> Maybe [cellResult] -> Maybe [cellResult]
-> (Markup -> MForm handler (FormResult [cellResult], FieldView UniWorX)) -> (Markup -> MForm handler (FormResult [cellResult], FieldView UniWorX))
massInputList field fieldSettings miButtonAction miIdent miSettings miRequired miPrevResult = over (mapped . _1 . mapped) (map snd . Map.elems) . massInput massInputList field fieldSettings onMissing miButtonAction miIdent miSettings miRequired miPrevResult = over (mapped . _1 . mapped) (map snd . Map.elems) . massInput
MassInput { miAdd = \_ _ _ submitBtn -> Just $ \csrf -> MassInput { miAdd = \_ _ _ submitBtn -> Just $ \csrf ->
return (FormSuccess $ \pRes -> FormSuccess $ Map.singleton (maybe 0 succ . fmap fst $ Map.lookupMax pRes) (), toWidget csrf >> fvInput submitBtn) return (FormSuccess $ \pRes -> FormSuccess $ Map.singleton (maybe 0 succ . fmap fst $ Map.lookupMax pRes) (), toWidget csrf >> fvWidget submitBtn)
, miCell = \pos () iRes nudge csrf -> , miCell = \pos () iRes nudge csrf ->
over _2 (\FieldView{..} -> $(widgetFile "widgets/massinput/list/cell")) <$> mreq field (fieldSettings pos & addName (nudge "field")) iRes over _2 (\fv -> $(widgetFile "widgets/massinput/list/cell")) <$> mreqMsg field (fieldSettings pos & addName (nudge "field")) onMissing iRes
, miDelete = miDeleteList , miDelete = miDeleteList
, miAllowAdd = \_ _ _ -> True , miAllowAdd = \_ _ _ -> True
, miAddEmpty = \_ _ _ -> Set.empty , miAddEmpty = \_ _ _ -> Set.empty
@ -484,20 +488,22 @@ massInputList field fieldSettings miButtonAction miIdent miSettings miRequired m
miRequired miRequired
(Map.fromList . zip [0..] . map ((), ) <$> miPrevResult) (Map.fromList . zip [0..] . map ((), ) <$> miPrevResult)
massInputListA :: forall handler cellResult ident. massInputListA :: forall handler cellResult ident msg.
( MonadHandler handler, HandlerSite handler ~ UniWorX ( MonadHandler handler, HandlerSite handler ~ UniWorX
, MonadThrow handler , MonadThrow handler
, PathPiece ident , PathPiece ident
, RenderMessage UniWorX msg
) )
=> Field handler cellResult => Field handler cellResult
-> (ListPosition -> FieldSettings UniWorX) -> (ListPosition -> FieldSettings UniWorX)
-> msg
-> (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX))
-> ident -> ident
-> FieldSettings UniWorX -> FieldSettings UniWorX
-> Bool -> Bool
-> Maybe [cellResult] -> Maybe [cellResult]
-> AForm handler [cellResult] -> AForm handler [cellResult]
massInputListA field fieldSettings miButtonAction miIdent miSettings miRequired miPrevResult = formToAForm . fmap (over _2 pure) $ massInputList field fieldSettings miButtonAction miIdent miSettings miRequired miPrevResult mempty massInputListA field fieldSettings onMissing miButtonAction miIdent miSettings miRequired miPrevResult = formToAForm . fmap (over _2 pure) $ massInputList field fieldSettings onMissing miButtonAction miIdent miSettings miRequired miPrevResult mempty
-- | Wrapper around `massInput` for the common case, that we just want a list of data with no option to modify it except deletion and addition -- | Wrapper around `massInput` for the common case, that we just want a list of data with no option to modify it except deletion and addition

View File

@ -1598,4 +1598,4 @@ dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty
where where
genForm _ mkUnique = do genForm _ mkUnique = do
(selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False) (selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False)
return (set selLens <$> selResult, [whamlet|^{fvInput selWidget}|]) return (set selLens <$> selResult, [whamlet|^{fvWidget selWidget}|])

View File

@ -18,6 +18,7 @@ import ClassyPrelude.Yesod as Import
, embed , embed
, try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_ , try, embed, catches, handle, catch, bracket, bracketOnError, bracket_, catchJust, finally, handleJust, mask, mask_, onException, tryJust, uninterruptibleMask, uninterruptibleMask_
, htmlField, fileField , htmlField, fileField
, mreq, areq, wreq -- Use `mreqMsg`, `areqMsg`, `wreqMsg`
) )
import UnliftIO.Async.Utils as Import import UnliftIO.Async.Utils as Import

View File

@ -4,7 +4,7 @@
module Utils.Form where module Utils.Form where
import ClassyPrelude.Yesod hiding (addMessage, addMessageI, cons, Proxy(..), identifyForm, addClass) import ClassyPrelude.Yesod hiding (addMessage, addMessageI, cons, Proxy(..), identifyForm, addClass, mreq, areq, wreq)
import qualified Yesod.Form.Functions as Yesod import qualified Yesod.Form.Functions as Yesod
import Yesod.Core.Instances () import Yesod.Core.Instances ()
import Settings import Settings
@ -409,7 +409,7 @@ buttonForm' btns csrf = do
$newline never $newline never
#{csrf} #{csrf}
$forall bView <- fViews $forall bView <- fViews
^{fvInput bView} ^{fvWidget bView}
|]) |])
withButtonForm' :: (MonadHandler m, Button (HandlerSite m) btn) withButtonForm' :: (MonadHandler m, Button (HandlerSite m) btn)
@ -932,6 +932,9 @@ formSection formSectionTitle = do
, fvInput = mempty , fvInput = mempty
}) })
fvWidget :: FieldView site -> WidgetFor site ()
fvWidget FieldView{..} = $(widgetFile "widgets/field-view/field-view")
------------------- -------------------
-- Special Forms -- -- Special Forms --
------------------- -------------------
@ -945,7 +948,7 @@ aformSection' = formToAForm . fmap (second pure) . formSection'
-- allows tooltips and arbitrary attributs. Section header must be given through `fsLabel` -- allows tooltips and arbitrary attributs. Section header must be given through `fsLabel`
formSection' :: (MonadHandler m, site ~ HandlerSite m, RenderMessage site FormMessage) => formSection' :: (MonadHandler m, site ~ HandlerSite m, RenderMessage site FormMessage) =>
FieldSettings site -> MForm m (FormResult (), FieldView site) -- TODO: WIP, delete FieldSettings site -> MForm m (FormResult (), FieldView site) -- TODO: WIP, delete
formSection' formSectionTitleSettings = mreq noinputField sectionSettings Nothing formSection' formSectionTitleSettings = Yesod.mreq noinputField sectionSettings Nothing
where where
sectionSettings = formSectionTitleSettings { fsId = Just idFormSectionNoinput } sectionSettings = formSectionTitleSettings { fsId = Just idFormSectionNoinput }
@ -1157,6 +1160,31 @@ wFormFields = mapRWST (fmap (\((a, s, w'), w) -> ((a, w), s, w')) . censor (cons
-- Special variants of @mopt@, @mreq@, ... -- -- Special variants of @mopt@, @mreq@, ... --
--------------------------------------------- ---------------------------------------------
data ValueRequired site = forall msg. RenderMessage site msg => ValueRequired msg
mreq :: forall m a.
( MonadHandler m
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
)
=> Field m a -> FieldSettings (HandlerSite m) -> Maybe a -> MForm m (FormResult a, FieldView (HandlerSite m))
mreq f fs@FieldSettings{..} mdef = mreqMsg f fs (ValueRequired fsLabel) mdef
wreq :: forall m a.
( MonadHandler m
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
)
=> Field m a -> FieldSettings (HandlerSite m) -> Maybe a -> WForm m (FormResult a)
wreq f fs@FieldSettings{..} mdef = wreqMsg f fs (ValueRequired fsLabel) mdef
areq :: forall m a.
( MonadHandler m
, RenderMessage (HandlerSite m) (ValueRequired (HandlerSite m))
)
=> Field m a -> FieldSettings (HandlerSite m) -> Maybe a -> AForm m a
areq f fs@FieldSettings{..} mdef = areqMsg f fs (ValueRequired fsLabel) mdef
mforced :: (site ~ HandlerSite m, MonadHandler m) mforced :: (site ~ HandlerSite m, MonadHandler m)
=> Field m a -> FieldSettings site -> a -> MForm m (FormResult a, FieldView site) => Field m a -> FieldSettings site -> a -> MForm m (FormResult a, FieldView site)
mforced Field{..} FieldSettings{..} val = do mforced Field{..} FieldSettings{..} val = do
@ -1223,13 +1251,13 @@ wforcedJust f fs (Just fDef) = wforced f fs fDef
wforcedJust _ _ Nothing = error "wforcedJust called with Nothing" wforcedJust _ _ Nothing = error "wforcedJust called with Nothing"
mpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) mpreq :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site) => Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
-- ^ Pseudo required -- ^ Pseudo required
-- --
-- `FieldView` has `fvRequired` set to `True` and @FormSuccess Nothing@ is cast to `FormFailure`. -- `FieldView` has `fvRequired` set to `True` and @FormSuccess Nothing@ is cast to `FormFailure`.
-- Otherwise acts exactly like `mopt`. -- Otherwise acts exactly like `mopt`.
mpreq f fs mx = do mpreq f fs@FieldSettings{..} mx = do
mr <- getMessageRender mr <- getMessageRender
(res, fv) <- mopt f fs (Just <$> mx) (res, fv) <- mopt f fs (Just <$> mx)
let fv' = fv { fvRequired = True } let fv' = fv { fvRequired = True }
@ -1237,22 +1265,33 @@ mpreq f fs mx = do
FormSuccess (Just res') FormSuccess (Just res')
-> (FormSuccess res', fv') -> (FormSuccess res', fv')
FormSuccess Nothing FormSuccess Nothing
-> (FormFailure [mr MsgValueRequired], fv' { fvErrors = Just . toHtml $ mr MsgValueRequired }) -> let errMsg = mr $ ValueRequired fsLabel
in ( FormFailure [errMsg]
, fv' { fvErrors = Just
[shamlet|
$newline never
$maybe pErr <- fvErrors fv'
#{pErr}
<br />
#{errMsg}
|]
}
)
FormFailure errs FormFailure errs
-> (FormFailure errs, fv') -> (FormFailure errs, fv')
FormMissing FormMissing
-> (FormMissing, fv') -> (FormMissing, fv')
apreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) apreq :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> AForm m a => Field m a -> FieldSettings site -> Maybe a -> AForm m a
apreq f fs mx = formToAForm $ over _2 pure <$> mpreq f fs mx apreq f fs mx = formToAForm $ over _2 pure <$> mpreq f fs mx
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) wpreq :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpreq f fs mx = mFormToWForm $ mpreq f fs mx wpreq f fs mx = mFormToWForm $ mpreq f fs mx
mpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) mpopt :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site) => Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
-- ^ Pseudo optional -- ^ Pseudo optional
-- --
@ -1260,10 +1299,10 @@ mpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
-- Otherwise acts exactly like `mreq`. -- Otherwise acts exactly like `mreq`.
mpopt f fs mx = set (_2 . _fvRequired) False <$> mreq f fs mx mpopt f fs mx = set (_2 . _fvRequired) False <$> mreq f fs mx
apopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) apopt :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> AForm m a => Field m a -> FieldSettings site -> Maybe a -> AForm m a
apopt f fs mx = formToAForm $ over _2 pure <$> mpopt f fs mx apopt f fs mx = formToAForm $ over _2 pure <$> mpopt f fs mx
wpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m) wpopt :: (RenderMessage site (ValueRequired site), HandlerSite m ~ site, MonadHandler m)
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a) => Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
wpopt f fs mx = mFormToWForm $ mpopt f fs mx wpopt f fs mx = mFormToWForm $ mpopt f fs mx

View File

@ -84,6 +84,7 @@ data Icon
| IconBreadcrumbSeparator | IconBreadcrumbSeparator
| IconMissingAllocationPriority | IconMissingAllocationPriority
| IconFileUploadSession | IconFileUploadSession
| IconStandaloneFieldError
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
iconText :: Icon -> Text iconText :: Icon -> Text
@ -146,6 +147,7 @@ iconText = \case
IconBreadcrumbSeparator -> "angle-right" IconBreadcrumbSeparator -> "angle-right"
IconMissingAllocationPriority -> "empty-set" IconMissingAllocationPriority -> "empty-set"
IconFileUploadSession -> "file-upload" IconFileUploadSession -> "file-upload"
IconStandaloneFieldError -> "exclamation"
instance Universe Icon instance Universe Icon
instance Finite Icon instance Finite Icon

View File

@ -7,7 +7,7 @@ $if is _Just muid
_{MsgAllocationPriority} _{MsgAllocationPriority}
<div .allocation-course__priority> <div .allocation-course__priority>
$maybe prioView <- mApplyFormView' >>= afvPriority $maybe prioView <- mApplyFormView' >>= afvPriority
^{fvInput prioView} ^{fvWidget prioView}
$nothing $nothing
_{MsgAllocationNoApplication} _{MsgAllocationNoApplication}
<a .allocation-course__name href=@{CourseR courseTerm courseSchool courseShorthand CShowR} target="_blank"> <a .allocation-course__name href=@{CourseR courseTerm courseSchool courseShorthand CShowR} target="_blank">

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=3> <td colspan=3>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput btn} ^{fvWidget btn}

View File

@ -6,4 +6,4 @@
<td> <td>
^{messageTooltip invWarnMsg} ^{messageTooltip invWarnMsg}
<td> <td>
^{fvInput lrwView} ^{fvWidget lrwView}

View File

@ -3,4 +3,4 @@ $newline never
#{csrf} #{csrf}
^{nameEmailWidget userEmail userDisplayName userSurname} # ^{nameEmailWidget userEmail userDisplayName userSurname} #
<td> <td>
^{fvInput lrwView} ^{fvWidget lrwView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td> <td>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=2> <td colspan=2>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=5> <td colspan=5>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -10,8 +10,8 @@ $case userIdent
^{identWidget} ^{identWidget}
<td> <td>
#{csrf} #{csrf}
^{fvInput stateView} ^{fvWidget stateView}
<td> <td>
^{fvInput byTutView} ^{fvWidget byTutView}
<td> <td>
^{fvInput propView} ^{fvWidget propView}

View File

@ -12,7 +12,7 @@ $newline never
<tr .massinput__cell .table__row> <tr .massinput__cell .table__row>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=2> <td colspan=2>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -28,7 +28,8 @@ $case formLayout
<div .form-group__input> <div .form-group__input>
^{fvInput view} ^{fvInput view}
$maybe err <- fvErrors view $maybe err <- fvErrors view
<div .form-error>#{err} <div .form-error>
#{err}
$if formHasRequiredFields $if formHasRequiredFields
<div .form-section-legend> <div .form-section-legend>
<span .form-group__required-marker> <span .form-group__required-marker>

View File

@ -2,10 +2,10 @@
<table> <table>
<tr> <tr>
<th>_{MsgCampusIdent} <th>_{MsgCampusIdent}
<td>^{fvInput identView} <td>^{fvWidget identView}
<tr> <tr>
<th>_{MsgCampusPassword} <th>_{MsgCampusPassword}
<td>^{fvInput passwordView} <td>^{fvWidget passwordView}
<tr> <tr>
<td colspan="2" style="text-align: right"> <td colspan="2" style="text-align: right">
<button type="submit">_{MsgCampusSubmit} <button type="submit">_{MsgCampusSubmit}

View File

@ -1,5 +1,5 @@
$newline never $newline never
<div .recipient-category__option-add.massinput__cell.massinput__cell--add> <div .recipient-category__option-add.massinput__cell.massinput__cell--add>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<div .recipient-category__option.massinput__cell> <div .recipient-category__option.massinput__cell>
#{csrf} #{csrf}
^{fvInput tickView} ^{fvWidget tickView}
<label .recipient-category__option-label for=#{fvId tickView}> <label .recipient-category__option-label for=#{fvId tickView}>
#{email} #{email}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<div .recipient-category__option.massinput__cell> <div .recipient-category__option.massinput__cell>
#{csrf} #{csrf}
^{fvInput tickView} ^{fvWidget tickView}
<label .recipient-category__option-label for=#{fvId tickView}> <label .recipient-category__option-label for=#{fvId tickView}>
#{nameHtml userDisplayName userSurname} #{nameHtml userDisplayName userSurname}

View File

@ -0,0 +1,7 @@
$newline never
<div .standalone-field .interactive-fieldset__target :fvRequired:.standalone-field--required :not fvRequired:.standalone-field--optional :is _Just fvErrors:.standalone-field--has-error>
<div .standalone-field__input>
^{fvInput}
$maybe err <- fvErrors
<div .standalone-field__error>
^{messageTooltip (Message Error err $ Just IconStandaloneFieldError)}

View File

@ -1,3 +1,3 @@
^{cellWdgt} ^{cellWdgt}
$maybe delWdgt <- fmap fvInput deleteButton $maybe delWdgt <- fmap fvWidget deleteButton
^{delWdgt} ^{delWdgt}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=3> <td colspan=3>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput btn} ^{fvWidget btn}

View File

@ -6,7 +6,7 @@ $newline never
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
$maybe delButton <- delButtons !? coord $maybe delButton <- delButtons !? coord
^{fvInput delButton} ^{fvWidget delButton}
$maybe addWdgt <- addWdgts !? (0, 0) $maybe addWdgt <- addWdgts !? (0, 0)
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=2> <td colspan=2>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=2> <td colspan=2>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput btn} ^{fvWidget btn}

View File

@ -6,7 +6,7 @@ $newline never
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
$maybe delButton <- delButtons !? coord $maybe delButton <- delButtons !? coord
^{fvInput delButton} ^{fvWidget delButton}
$maybe addWdgt <- addWdgts !? (0, 0) $maybe addWdgt <- addWdgts !? (0, 0)
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>

View File

@ -1,4 +1,4 @@
$newline never $newline never
^{formWidget} ^{formWidget}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -1,5 +1,8 @@
$newline never $newline never
<td>#{csrf}^{fvInput epfIdView}^{fvInput epfNumberView} <td>
<td>^{fvInput epfNameView} #{csrf}
<td>^{fvInput epfMaxPointsView} ^{fvInput epfIdView}
<td>^{fvInput epfWeightView} ^{fvWidget epfNumberView}
<td>^{fvWidget epfNameView}
<td>^{fvWidget epfMaxPointsView}
<td>^{fvWidget epfWeightView}

View File

@ -25,7 +25,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,4 +1,4 @@
$newline never $newline never
^{formWidget} ^{formWidget}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -1,7 +1,7 @@
$newline never $newline never
<td>#{csrf}^{fvInput eofIdView}^{fvInput eofNameView} <td>#{csrf}^{fvInput eofIdView}^{fvWidget eofNameView}
<td>^{fvInput eofRoomView} <td>^{fvWidget eofRoomView}
<td>^{fvInput eofCapacityView} <td>^{fvWidget eofCapacityView}
<td>^{fvInput eofStartView} <td>^{fvWidget eofStartView}
<td>^{fvInput eofEndView} <td>^{fvWidget eofEndView}
<td>^{fvInput eofDescView} <td>^{fvWidget eofDescView}

View File

@ -22,7 +22,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,3 +1,3 @@
$newline never $newline never
#{csrf} #{csrf}
^{fvInput} ^{fvWidget fv}

View File

@ -5,6 +5,6 @@ $newline never
<div .massinput-list__field> <div .massinput-list__field>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<div .massinput-list__delete> <div .massinput-list__delete>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<div .massinput-list__add .massinput__cell .massinput__cell--add> <div .massinput-list__add .massinput__cell .massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td colspan=2> <td colspan=2>
#{csrf} #{csrf}
^{fvInput addView} ^{fvWidget addView}
<td> <td>
^{fvInput btn} ^{fvWidget btn}

View File

@ -6,7 +6,7 @@ $newline never
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
$maybe delButton <- delButtons !? coord $maybe delButton <- delButtons !? coord
^{fvInput delButton} ^{fvWidget delButton}
$maybe addWdgt <- addWdgts !? (0, 0) $maybe addWdgt <- addWdgts !? (0, 0)
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>

View File

@ -1,7 +1,7 @@
$newline never $newline never
<td> <td>
#{csrf} #{csrf}
^{fvInput routeView}<br /> ^{fvWidget routeView}<br />
^{fvInput restrView} ^{fvWidget restrView}
<td> <td>
^{fvInput btn} ^{fvWidget btn}

View File

@ -1,6 +1,6 @@
$newline never $newline never
<td> <td>
#{csrf} #{csrf}
^{fvInput routeView}<br /> ^{fvWidget routeView}<br />
^{fvInput restrView} ^{fvWidget restrView}
<td> <td>

View File

@ -6,7 +6,7 @@ $newline never
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
$maybe delButton <- delButtons !? coord $maybe delButton <- delButtons !? coord
^{fvInput delButton} ^{fvWidget delButton}
$maybe addWdgt <- addWdgts !? (0, 0) $maybe addWdgt <- addWdgts !? (0, 0)
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>

View File

@ -1,4 +1,4 @@
$newline never $newline never
^{formWidget} ^{formWidget}
<td> <td>
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -1,4 +1,4 @@
$newline never $newline never
<td>#{csrf}^{fvInput labelView} <td>#{csrf}^{fvWidget labelView}
<td>^{fvInput nameView} <td>^{fvWidget nameView}
<td>^{fvInput reqView} <td>^{fvWidget reqView}

View File

@ -10,7 +10,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
<tfoot> <tfoot>
<tr .massinput__cell.massinput__cell--add> <tr .massinput__cell.massinput__cell--add>
^{addWdgts ! (0, 0)} ^{addWdgts ! (0, 0)}

View File

@ -2,4 +2,4 @@ $newline never
<td colspan=2> <td colspan=2>
^{addWidget} ^{addWidget}
<td style="vertical-align: bottom"> <td style="vertical-align: bottom">
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
$if null (review liveCoords lLength) $if null (review liveCoords lLength)
<tr> <tr>
<td colspan=3 style="font-weight: 600; font-size: 0.9rem; color: var(--color-fontsec);"> <td colspan=3 style="font-weight: 600; font-size: 0.9rem; color: var(--color-fontsec);">

View File

@ -2,4 +2,4 @@ $newline never
<td colspan=2> <td colspan=2>
^{addWidget} ^{addWidget}
<td style="vertical-align: bottom"> <td style="vertical-align: bottom">
^{fvInput submitView} ^{fvWidget submitView}

View File

@ -5,7 +5,7 @@ $newline never
<tr .massinput__cell> <tr .massinput__cell>
^{cellWdgts ! coord} ^{cellWdgts ! coord}
<td> <td>
^{fvInput (delButtons ! coord)} ^{fvWidget (delButtons ! coord)}
$if null (review liveCoords lLength) $if null (review liveCoords lLength)
<tr> <tr>
<td colspan=3 style="font-weight: 600; font-size: 0.9rem; color: var(--color-fontsec);"> <td colspan=3 style="font-weight: 600; font-size: 0.9rem; color: var(--color-fontsec);">

View File

@ -4,11 +4,11 @@ $# extra protects us against CSRF
$# Maybe display textField for passcode $# Maybe display textField for passcode
$maybe secretView <- msecretView $maybe secretView <- msecretView
^{fvLabel secretView} ^{fvLabel secretView}
^{fvInput secretView} ^{fvWidget secretView}
$# Ask for associated primary field uf study, unless registered $# Ask for associated primary field uf study, unless registered
$maybe sfView <- msfView $maybe sfView <- msfView
^{fvLabel sfView} ^{fvLabel sfView}
^{fvInput sfView} ^{fvWidget sfView}
$# Always display register/deregister button $# Always display register/deregister button
^{fvInput btnView} ^{fvWidget btnView}

View File

@ -16,4 +16,4 @@ $newline never
$forall function <- allFunctions $forall function <- allFunctions
<td .table__td> <td .table__td>
$maybe (_, boxView) <- Map.lookup (function, sid) boxRights $maybe (_, boxView) <- Map.lookup (function, sid) boxRights
^{fvInput boxView} ^{fvWidget boxView}