Merge branch 'master' into fix/split-message-files

This commit is contained in:
Winnie Ros 2021-04-14 09:14:36 +02:00
commit 690872d679
27 changed files with 497 additions and 135 deletions

View File

@ -194,3 +194,8 @@
- warn: { lhs: n >= length xs, rhs: maxLength n xs, note: IncreasesLaziness } - warn: { lhs: n >= length xs, rhs: maxLength n xs, note: IncreasesLaziness }
- warn: { lhs: length xs < n, rhs: maxLength (n - 1) xs, note: IncreasesLaziness } - warn: { lhs: length xs < n, rhs: maxLength (n - 1) xs, note: IncreasesLaziness }
- warn: { lhs: n > length xs, rhs: maxLength (n - 1) xs, note: IncreasesLaziness } - warn: { lhs: n > length xs, rhs: maxLength (n - 1) xs, note: IncreasesLaziness }
- error: {lhs: v Database.Esqueleto.==. Database.Esqueleto.nothing, rhs: Database.Esqueleto.isNothing v, name: Use Esqueleto's isNothing}
- error: {lhs: v Database.Esqueleto.==. Database.Esqueleto.val Nothing, rhs: Database.Esqueleto.isNothing v, name: Use Esqueleto's isNothing}
- error: {lhs: v Database.Esqueleto.!=. Database.Esqueleto.nothing, rhs: Database.Esqueleto.Utils.isJust v, name: Use Esqueleto's not isNothing}
- error: {lhs: v Database.Esqueleto.!=. Database.Esqueleto.val Nothing, rhs: Database.Esqueleto.Utils.isJust v, name: Use Esqueleto's not isNothing}

View File

@ -2,6 +2,21 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [25.9.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.9.0...v25.9.1) (2021-04-14)
## [25.9.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.8.1...v25.9.0) (2021-04-13)
### Features
* partial support for lsf import ([37cdc77](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/37cdc775b5b2d3e4cd1cc22858b2c05e75de8a3c)), closes [#686](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/686)
### Bug Fixes
* build ([5c709f1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/5c709f1bbb077d981fbd5d59e9c0f30cddbb468d))
* prevent deleting sheet-referenced exam parts ([9859c2e](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/commit/9859c2e99c1e0c7531ee38864a24ff279a8e6a7c)), closes [#681](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/issues/681)
## [25.8.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.8.0...v25.8.1) (2021-04-09) ## [25.8.1](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.8.0...v25.8.1) (2021-04-09)
## [25.8.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.7.0...v25.8.0) (2021-04-08) ## [25.8.0](https://gitlab2.rz.ifi.lmu.de/uni2work/uni2work/compare/v25.7.0...v25.8.0) (2021-04-08)

View File

@ -221,7 +221,7 @@ cookies:
secure: "_env:COOKIES_SECURE:true" secure: "_env:COOKIES_SECURE:true"
user-defaults: user-defaults:
max-favourites: 12 max-favourites: 0
max-favourite-terms: 2 max-favourite-terms: 2
theme: Default theme: Default
date-time-format: "%a %d %b %Y %R" date-time-format: "%a %d %b %Y %R"

View File

@ -165,7 +165,7 @@ h4
margin-top: var(--current-header-height) margin-top: var(--current-header-height)
margin-left: 0 margin-left: 0
:target:not(table :target)::before :target:not(table, .show-hide__toggle)::before
content: "" content: ""
display: block display: block
height: var(--current-header-height) height: var(--current-header-height)
@ -275,6 +275,9 @@ button:not(.btn-link),
display: grid display: grid
grid: min-content / auto-flow max-content grid: min-content / auto-flow max-content
.buttongroup--inline
display: inline-grid
input[type="submit"][disabled]:not(.btn-link), input[type="submit"][disabled]:not(.btn-link),
input[type="button"][disabled]:not(.btn-link), input[type="button"][disabled]:not(.btn-link),
button[disabled]:not(.btn-link), button[disabled]:not(.btn-link),
@ -328,6 +331,10 @@ input[type="button"].btn-info:not(.btn-link):hover,
&:not([disabled]):hover &:not([disabled]):hover
color: var(--color-link-hover) color: var(--color-link-hover)
// STACK ICON STYLE
.icon--stacked
font-size: 0.5rem
// GENERAL TABLE STYLES // GENERAL TABLE STYLES
.table .table
margin: 21px 0 margin: 21px 0

View File

@ -1,3 +1,6 @@
@use "../../common" as *
@use "../../app" as *
.main__aside .main__aside
position: fixed position: fixed
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3) box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
@ -90,6 +93,18 @@
padding: 0 13px padding: 0 13px
margin: 3px 0 margin: 3px 0
.asidenav__box-explanation
@extend .explanation
padding: 0 13px
margin: 3px 0
opacity: .66
font-size: .7rem
/* transition: opacity .2s ease, font-size .2s ease
/* &:hover
/* font-size: .9rem
/* opacity: 1
// LOGO // LOGO
.asidenav__logo .asidenav__logo
@ -217,9 +232,30 @@
.asidenav__link-shorthand .asidenav__link-shorthand
display: none display: none
.asidenav__link-favourite-toggle
opacity: .33
&:hover
opacity: 1
button
display: flex
text-decoration: none
.asidenav__link-label .asidenav__link-label
display: flex
justify-content: space-between
align-items: center
line-height: 1 line-height: 1
& > .asidenav__link-label-text
word-break: break-word
flex: 1 1 auto
& > .asidenav__link-favourite-toggle
flex: 0 0 $fa-fw-width
margin: 0 5px
// hover sub-menus // hover sub-menus
.asidenav__nested-list-wrapper .asidenav__nested-list-wrapper
position: absolute position: absolute

View File

@ -83,7 +83,6 @@ CosubmittorTip: Einladungen per E-Mail erhalten genau jene Adressen, für die ni
CorrectionsTitle: Zugewiesene Korrekturen CorrectionsTitle: Zugewiesene Korrekturen
CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName} CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName}
<<<<<<< Updated upstream
MaterialName: Name MaterialName: Name
MaterialType: Art MaterialType: Art
MaterialTypePlaceholder: Folien, Code, Beispiel, ... MaterialTypePlaceholder: Folien, Code, Beispiel, ...
@ -180,8 +179,6 @@ MaterialFree: Kursmaterialien ohne Anmeldung zugänglich
UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung
UnauthorizedSystemMessageTime: Diese Systemnachricht ist noch nicht oder nicht mehr einsehbar. UnauthorizedSystemMessageTime: Diese Systemnachricht ist noch nicht oder nicht mehr einsehbar.
UnauthorizedSystemMessageAuth: Diese Systemnachricht ist nur für angemeldete Benutzer einsehbar. UnauthorizedSystemMessageAuth: Diese Systemnachricht ist nur für angemeldete Benutzer einsehbar.
=======
>>>>>>> Stashed changes
UnsupportedAuthPredicate authTagT@Text shownRoute@Text: „#{authTagT}“ wurde auf eine Route angewandt, die dies nicht unterstützt: „#{shownRoute}“ UnsupportedAuthPredicate authTagT@Text shownRoute@Text: „#{authTagT}“ wurde auf eine Route angewandt, die dies nicht unterstützt: „#{shownRoute}“
WorkflowRoleUserMismatch: Sie sind nicht einer der vom Workflow geforderten Benutzer WorkflowRoleUserMismatch: Sie sind nicht einer der vom Workflow geforderten Benutzer
@ -491,12 +488,9 @@ CsvDeleteMissing: Fehlende Einträge entfernen
TableProportion c@Text of'@Text prop@Rational: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%) TableProportion c@Text of'@Text prop@Rational: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c@Text of'@Text: #{c}/#{of'} TableProportionNoRatio c@Text of'@Text: #{c}/#{of'}
<<<<<<< Updated upstream
ParticipantsCsvName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-kursteilnehmer ParticipantsCsvName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-kursteilnehmer
ParticipantsCsvSheetName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Kursteilnehmer ParticipantsCsvSheetName tid@TermId ssh@SchoolId: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Kursteilnehmer
=======
>>>>>>> Stashed changes
CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt
CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
@ -581,8 +575,13 @@ FavouriteParticipant: Ihre Kurse
FavouriteManual: Favoriten FavouriteManual: Favoriten
FavouriteCurrent: Aktueller Kurs FavouriteCurrent: Aktueller Kurs
<<<<<<< HEAD
======= =======
>>>>>>> Stashed changes >>>>>>> Stashed changes
=======
FavouritesEmptyTip: Hier werden Ihre Kurse, sowie zuletzt besuchte Kurse angezeigt.
FavouritesToggleTip: Der Anzeigemodus für den aktuellen Kurs kann über einen Klick auf das Stern-Symbol zwischen automatisch, permanent und nie gewechselt werden.
>>>>>>> master
FavouritesUnavailableTip: Das Schnellzugriffsmenü für diesen Kurs ist aktuell nicht verfügbar. FavouritesUnavailableTip: Das Schnellzugriffsmenü für diesen Kurs ist aktuell nicht verfügbar.
@ -669,3 +668,15 @@ UrlFieldCouldNotParseAbsolute: Konnte nicht als absolute URL interpretiert werde
WGFTextInput: Textfeld WGFTextInput: Textfeld
WGFFileUpload: Dateifeld WGFFileUpload: Dateifeld
<<<<<<< HEAD
=======
WorkflowGraphFormUploadIsDirectory: Upload ist Verzeichnis
WorkflowGraphFormInvalidNumberOfFiles: Es muss genau eine Datei hochgeladen werden
CourseSortingOnlyLoggedIn: Das Benutzerinterface zur Sortierung dieser Tabelle ist nur für eingeloggte Benutzer aktiv
CorrectionInvisibleExamUnfinished: Die Frist „_{MsgExamFinished}“ für die relevante Prüfung ist noch nicht verstrichen
CorrectionInvisibleRatingNotDone: Die Bewertung ist nicht als „Abgeschlossen“ markiert
CorrectionInvisibleWarning: Die Bewertung dieser Abgabe ist aktuell für mindestens eine an der Abgabe beteiligte Person nicht sichtbar!
CorrectionInvisibleReasons: Mögliche Gründe hierfür:
>>>>>>> master

View File

@ -86,7 +86,6 @@ CosubmittorTip: Invitations are sent via email to exactly those addresses for wh
CorrectionsTitle: Assigned corrections CorrectionsTitle: Assigned corrections
CorrectorsHead sheetName: Correctors for #{sheetName} CorrectorsHead sheetName: Correctors for #{sheetName}
<<<<<<< Updated upstream
MaterialName: Name MaterialName: Name
MaterialType: Type MaterialType: Type
MaterialTypePlaceholder: Slides, Code, Example, ... MaterialTypePlaceholder: Slides, Code, Example, ...
@ -182,8 +181,6 @@ MaterialFree: Course material is publicly available.
UnauthorizedWrite: You do not have the write permission necessary to perform this action UnauthorizedWrite: You do not have the write permission necessary to perform this action
UnauthorizedSystemMessageTime: This system-message is not currently available. UnauthorizedSystemMessageTime: This system-message is not currently available.
UnauthorizedSystemMessageAuth: This system-message is only available to logged in users. UnauthorizedSystemMessageAuth: This system-message is only available to logged in users.
=======
>>>>>>> Stashed changes
UnsupportedAuthPredicate authTagT shownRoute: “#{authTagT}” was applied to a route which does not support it: “#{shownRoute}” UnsupportedAuthPredicate authTagT shownRoute: “#{authTagT}” was applied to a route which does not support it: “#{shownRoute}”
WorkflowRoleUserMismatch: You aren't any of the users authorized by the workflow WorkflowRoleUserMismatch: You aren't any of the users authorized by the workflow
@ -493,12 +490,9 @@ CsvDeleteMissing: Delete missing entries
TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%) TableProportion c of' prop: #{c}/#{of'} (#{rationalToFixed2 (100 * prop)}%)
TableProportionNoRatio c of': #{c}/#{of'} TableProportionNoRatio c of': #{c}/#{of'}
<<<<<<< Updated upstream
ParticipantsCsvName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-participants ParticipantsCsvName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-participants
ParticipantsCsvSheetName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Participants ParticipantsCsvSheetName tid ssh: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)} Participants
=======
>>>>>>> Stashed changes
CsvColumnUserField: Field of study the participant specified when enrolling for the course CsvColumnUserField: Field of study the participant specified when enrolling for the course
CsvColumnUserDegree: Degree the participant pursues in their associated field of study CsvColumnUserDegree: Degree the participant pursues in their associated field of study
CsvColumnUserSemester: Semester the participant is in wrt. to their associated field of study CsvColumnUserSemester: Semester the participant is in wrt. to their associated field of study
@ -583,8 +577,13 @@ FavouriteParticipant: Your courses
FavouriteManual: Favourites FavouriteManual: Favourites
FavouriteCurrent: Current course FavouriteCurrent: Current course
<<<<<<< HEAD
======= =======
>>>>>>> Stashed changes >>>>>>> Stashed changes
=======
FavouritesEmptyTip: Your courses and recently visited courses are shown here.
FavouritesToggleTip: The display mode for the current course can be changed between automatic, permanent and never with a click on the star symbol.
>>>>>>> master
FavouritesUnavailableTip: Quick Actions for this course are currently not available. FavouritesUnavailableTip: Quick Actions for this course are currently not available.
UserSimplifiedFeaturesOfStudyCsv: Simplified features of study UserSimplifiedFeaturesOfStudyCsv: Simplified features of study

2
package-lock.json generated
View File

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

View File

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

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 25.8.1 version: 25.9.1
dependencies: dependencies:
- base - base
- yesod - yesod

2
routes
View File

@ -174,7 +174,7 @@
!/course/new CourseNewR GET POST !lecturer !/course/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: /course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !tutor !corrector !exam-corrector !course-registered !course-time !evaluation !exam-office !allocation-admin / CShowR GET !tutor !corrector !exam-corrector !course-registered !course-time !evaluation !exam-office !allocation-admin
/favourite CFavouriteR POST /favourite CFavouriteR GET POST !free
/register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬course-registeredANDcourse-time !timeANDallocation-timeAND¬exam-resultANDcourse-registered !lecturerANDallocation-time /register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬course-registeredANDcourse-time !timeANDallocation-timeAND¬exam-resultANDcourse-registered !lecturerANDallocation-time
/register-template CRegisterTemplateR GET !course-time /register-template CRegisterTemplateR GET !course-time
/edit CEditR GET POST /edit CEditR GET POST

View File

@ -32,6 +32,7 @@ module Database.Esqueleto.Utils
, selectMaybe , selectMaybe
, day, diffDays, diffTimes , day, diffDays, diffTimes
, exprLift , exprLift
, explicitUnsafeCoerceSqlExprValue
, module Database.Esqueleto.Utils.TH , module Database.Esqueleto.Utils.TH
) where ) where
@ -55,6 +56,8 @@ import Data.Coerce (Coercible)
import Data.Time.Clock (NominalDiffTime) import Data.Time.Clock (NominalDiffTime)
import qualified Data.Text.Lazy.Builder as Text.Builder
{-# ANN any ("HLint: ignore Use any" :: String) #-} {-# ANN any ("HLint: ignore Use any" :: String) #-}
{-# ANN all ("HLint: ignore Use all" :: String) #-} {-# ANN all ("HLint: ignore Use all" :: String) #-}
@ -130,6 +133,17 @@ substring (E.ERaw p1 f1) (E.ERaw p2 f2) (E.ERaw p3 f3)
) )
substring a b c = substring (construct a) (construct b) (construct c) substring a b c = substring (construct a) (construct b) (construct c)
explicitUnsafeCoerceSqlExprValue :: forall b a.
Text
-> E.SqlExpr (E.Value a)
-> E.SqlExpr (E.Value b)
explicitUnsafeCoerceSqlExprValue typ (E.ERaw p1 f1) = E.ERaw E.Parens $ \info ->
let (valTLB, valVals) = f1 info
in ( E.parensM p1 valTLB <> " :: " <> Text.Builder.fromText typ
, valVals
)
explicitUnsafeCoerceSqlExprValue typ val = explicitUnsafeCoerceSqlExprValue typ $ construct val
construct :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a) construct :: E.SqlExpr (E.Value a) -> E.SqlExpr (E.Value a)
construct (E.ERaw p f) = E.ERaw E.Parens $ \info -> construct (E.ERaw p f) = E.ERaw E.Parens $ \info ->
let (b1, vals) = f info let (b1, vals) = f info

View File

@ -10,7 +10,6 @@ module Foundation.Instances
import Import.NoFoundation import Import.NoFoundation
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.List as List
import Data.List (inits) import Data.List (inits)
import qualified Yesod.Core.Unsafe as Unsafe import qualified Yesod.Core.Unsafe as Unsafe
@ -28,6 +27,7 @@ import qualified Foundation.Yesod.StaticContent as UniWorX
import qualified Foundation.Yesod.Persist as UniWorX import qualified Foundation.Yesod.Persist as UniWorX
import qualified Foundation.Yesod.Auth as UniWorX import qualified Foundation.Yesod.Auth as UniWorX
import Foundation.Instances.ButtonClass
import Foundation.SiteLayout import Foundation.SiteLayout
import Foundation.Type import Foundation.Type
import Foundation.I18n import Foundation.I18n
@ -51,29 +51,6 @@ import qualified Data.CaseInsensitive as CI
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
data instance ButtonClass UniWorX
= BCIsButton
| BCDefault
| BCPrimary
| BCSuccess
| BCInfo
| BCWarning
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance PathPiece (ButtonClass UniWorX) where
toPathPiece BCIsButton = "btn"
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
instance Button UniWorX ButtonSubmit where
btnClasses BtnSubmit = [BCIsButton, BCPrimary]
-- Please see the documentation for the Yesod typeclass. There are a number -- Please see the documentation for the Yesod typeclass. There are a number
-- of settings which can be configured by overriding methods here. -- of settings which can be configured by overriding methods here.
instance Yesod UniWorX where instance Yesod UniWorX where

View File

@ -0,0 +1,33 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Foundation.Instances.ButtonClass (ButtonClass(..)) where
import Import.NoFoundation
import Utils.Form
import Foundation.Type
import qualified Data.List as List
-- instance RenderMessage UniWorX ButtonSubmit
import Foundation.I18n ()
data instance ButtonClass UniWorX
= BCIsButton
| BCDefault
| BCPrimary
| BCSuccess
| BCInfo
| BCWarning
| BCDanger
| BCLink
| BCMassInputAdd | BCMassInputDelete
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
deriving anyclass (Universe, Finite)
instance PathPiece (ButtonClass UniWorX) where
toPathPiece BCIsButton = "btn"
toPathPiece bClass = ("btn-" <>) . camelToPathPiece' 1 $ tshow bClass
fromPathPiece = flip List.lookup $ map (toPathPiece &&& id) universeF
instance Button UniWorX ButtonSubmit where
btnClasses BtnSubmit = [BCIsButton, BCPrimary]

View File

@ -5,6 +5,7 @@ module Foundation.SiteLayout
( siteLayout', siteLayout ( siteLayout', siteLayout
, siteLayoutMsg', siteLayoutMsg , siteLayoutMsg', siteLayoutMsg
, getSystemMessageState , getSystemMessageState
, storedFavouriteReason
) where ) where
import Import.NoFoundation hiding (embedFile, runDB) import Import.NoFoundation hiding (embedFile, runDB)
@ -15,6 +16,7 @@ import Foundation.Routes
import Foundation.Navigation import Foundation.Navigation
import Foundation.I18n import Foundation.I18n
import Foundation.Yesod.Persist import Foundation.Yesod.Persist
import Foundation.Instances.ButtonClass
import Utils.SystemMessage import Utils.SystemMessage
import Utils.Form import Utils.Form
@ -37,6 +39,61 @@ import Text.Cassius (cassiusFile)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Data.FileEmbed (embedFile) import Data.FileEmbed (embedFile)
data CourseFavouriteToggleButton
= BtnCourseFavouriteToggleManual
| BtnCourseFavouriteToggleAutomatic
| BtnCourseFavouriteToggleOff
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
deriving anyclass (Universe, Finite)
nullaryPathPiece ''CourseFavouriteToggleButton $ camelToPathPiece' 4
instance Button UniWorX CourseFavouriteToggleButton where
btnLabel BtnCourseFavouriteToggleManual
= toWidget $ iconFixed IconCourseFavouriteManual
btnLabel BtnCourseFavouriteToggleAutomatic
= toWidget $ iconFixed IconCourseFavouriteAutomatic
btnLabel BtnCourseFavouriteToggleOff
= toWidget $ iconStacked IconCourseFavouriteManual IconCourseFavouriteOff
btnClasses _ = [BCIsButton, BCLink]
-- inspired by examAutoOccurrenceIgnoreRoomsForm
courseFavouriteToggleForm :: Maybe FavouriteReason -> Form ()
courseFavouriteToggleForm currentReason html
= over _1 void <$> identifyForm FIDCourseFavouriteToggle (buttonForm' [btn]) html
where
btn :: CourseFavouriteToggleButton
btn = case currentReason of
Nothing -> BtnCourseFavouriteToggleOff
(Just FavouriteVisited) -> BtnCourseFavouriteToggleAutomatic
(Just FavouriteParticipant) -> BtnCourseFavouriteToggleAutomatic
(Just FavouriteManual) -> BtnCourseFavouriteToggleManual
(Just FavouriteCurrent) -> BtnCourseFavouriteToggleAutomatic
-- (storedReason, isBlacklist)
-- Will never return FavouriteCurrent
-- Nothing if no entry for current user (e.g. not logged in)
storedFavouriteReason :: (MonadIO m, BearerAuthSite UniWorX) => TermId -> SchoolId -> CourseShorthand -> Maybe (AuthId UniWorX, AuthEntity UniWorX)
-> ReaderT SqlBackend m (Maybe (Maybe FavouriteReason, Bool))
storedFavouriteReason tid ssh csh muid = fmap unValueFirst . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do
E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.limit 1 -- we know that there is at most one match, but we tell the DB this info too
let isBlacklist = E.exists . E.from $ \courseNoFavourite ->
E.where_ $ E.just (courseNoFavourite E.^. CourseNoFavouriteUser) E.==. E.val (view _1 <$> muid)
E.&&. courseNoFavourite E.^. CourseNoFavouriteCourse E.==. course E.^. CourseId
reason :: (E.SqlExpr (E.Value (Maybe FavouriteReason)), E.SqlExpr (E.Value Bool))
reason = (courseFavourite E.?. CourseFavouriteReason, isBlacklist)
pure reason
where
unValueFirst :: [(E.Value (Maybe a), E.Value Bool)] -> Maybe (Maybe a, Bool)
-- `over each E.unValue` doesn't work here, since E.unValue is monomorphised
unValueFirst = fmap (bimap E.unValue E.unValue) . listToMaybe
data MemcachedKeyFavourites data MemcachedKeyFavourites
= MemcachedKeyFavouriteQuickActions (TermId, SchoolId, CourseShorthand) AuthContext (NonEmpty Lang) = MemcachedKeyFavouriteQuickActions (TermId, SchoolId, CourseShorthand) AuthContext (NonEmpty Lang)
@ -54,16 +111,15 @@ data MemcachedLimitKeyFavourites
deriving anyclass (Hashable, Binary) deriving anyclass (Hashable, Binary)
siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, Button UniWorX ButtonSubmit) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html siteLayoutMsg :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg = siteLayout . i18n siteLayoutMsg = siteLayout . i18n
{-# DEPRECATED siteLayoutMsg' "Use siteLayoutMsg" #-} {-# DEPRECATED siteLayoutMsg' "Use siteLayoutMsg" #-}
siteLayoutMsg' :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend, Button UniWorX ButtonSubmit) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html siteLayoutMsg' :: (RenderMessage site msg, site ~ UniWorX, BearerAuthSite UniWorX, YesodPersistBackend UniWorX ~ SqlBackend) => msg -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
siteLayoutMsg' = siteLayoutMsg siteLayoutMsg' = siteLayoutMsg
siteLayout :: ( BearerAuthSite UniWorX siteLayout :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend , YesodPersistBackend UniWorX ~ SqlBackend
, Button UniWorX ButtonSubmit
) )
=> WidgetFor UniWorX () -- ^ `pageHeading` => WidgetFor UniWorX () -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -71,7 +127,6 @@ siteLayout = siteLayout' . Just
siteLayout' :: ( BearerAuthSite UniWorX siteLayout' :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend , YesodPersistBackend UniWorX ~ SqlBackend
, Button UniWorX ButtonSubmit
) )
=> Maybe (WidgetFor UniWorX ()) -- ^ `pageHeading` => Maybe (WidgetFor UniWorX ()) -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html -> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -94,11 +149,11 @@ siteLayout' overrideHeading widget = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
muid <- maybeAuthPair
-- Lookup Favourites, Breadcrumbs, Headline, & Theme if possible -- Lookup Favourites, Breadcrumbs, Headline, & Theme if possible
(favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme) <- do (favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme, storedReasonAndToggleRoute) <- do
muid <- maybeAuthPair
(favCourses, breadcrumbs'', nav', contentHeadline, mmsgs) <- runDB $ do (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs, storedReasonAndToggleRoute) <- runDB $ do
favCourses'' <- withReaderT (projectBackend @SqlReadBackend) . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do favCourses'' <- withReaderT (projectBackend @SqlReadBackend) . E.select . E.from $ \(course `E.LeftOuterJoin` courseFavourite) -> do
E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid) E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
@ -189,8 +244,12 @@ siteLayout' overrideHeading widget = do
forM_ authTagPivots $ forM_ authTagPivots $
\authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute])) \authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute]))
getMessages getMessages
storedReasonAndToggleRoute <- case mcurrentRoute of
(Just (CourseR tid ssh csh _)) -> (, Just . SomeRoute $ CourseR tid ssh csh CFavouriteR) <$> storedFavouriteReason tid ssh csh muid
_otherwise -> pure (Nothing, Nothing)
return (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs) return (favCourses, breadcrumbs'', nav', contentHeadline, mmsgs, storedReasonAndToggleRoute)
return ( favCourses return ( favCourses
, breadcrumbs'' , breadcrumbs''
@ -199,10 +258,37 @@ siteLayout' overrideHeading widget = do
, mmsgs , mmsgs
, maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid , maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ view _2 <$> muid
, maybe userDefaultTheme userTheme $ view _2 <$> muid , maybe userDefaultTheme userTheme $ view _2 <$> muid
, storedReasonAndToggleRoute
) )
let (currentReason', maybeRoute) = storedReasonAndToggleRoute
currentReason = case currentReason' of
-- (reason, blacklist)
(Just (_reason, True)) -> Nothing
(Just (Just reason, False)) -> Just reason
(Just (Nothing, False)) -> Just FavouriteCurrent
Nothing -> Just FavouriteCurrent
showFavToggle :: FavouriteReason -> Bool
showFavToggle FavouriteCurrent = isJust muid
showFavToggle _favouriteReason = False
favouriteToggleRes <- runFormPost $ courseFavouriteToggleForm currentReason
let favouriteToggleWgt = favouriteToggleRes & \((_, favouriteToggleView), favouriteToggleEncoding) ->
wrapForm favouriteToggleView def
{ formAction = maybeRoute
, formEncoding = favouriteToggleEncoding
, formSubmit = FormNoSubmit
, formAttrs = [("class", "buttongroup buttongroup--inline")]
}
let favouriteTerms :: [TermIdentifier] let favouriteTerms :: [TermIdentifier]
favouriteTerms = take maxFavouriteTerms . Set.toDescList $ foldMap (\((_, tid, _, _), _, _, _, _) -> Set.singleton $ unTermKey tid) favourites' favouriteTerms = Set.toDescList . prune $ toTermKeySet favourites'
where
prune ts = currentTerms `Set.union` setTakeEnd (maxFavouriteTerms - Set.size currentTerms) (ts `Set.difference` currentTerms)
setTakeEnd n ts
| n <= 0 = Set.empty
| otherwise = Set.drop (Set.size ts - n) ts
currentTerms = toTermKeySet $ filter (views (_2 . _Value) . maybe True $ is _FavouriteCurrent) favourites'
toTermKeySet = setOf $ folded . _1 . _2 . to unTermKey
favourites <- fmap catMaybes . forM favourites' $ \(c@(_, tid, ssh, csh), E.Value mFavourite, courseVisible, mayView, mayEdit) favourites <- fmap catMaybes . forM favourites' $ \(c@(_, tid, ssh, csh), E.Value mFavourite, courseVisible, mayView, mayEdit)
-> let courseRoute = CourseR tid ssh csh CShowR -> let courseRoute = CourseR tid ssh csh CShowR
@ -251,6 +337,8 @@ siteLayout' overrideHeading widget = do
favouriteTermReason tid favReason' = favourites favouriteTermReason tid favReason' = favourites
& filter (\((_, tid', _, _), _, _, favReason, _, _, _) -> unTermKey tid' == tid && favReason == favReason') & filter (\((_, tid', _, _), _, _, favReason, _, _, _) -> unTermKey tid' == tid && favReason == favReason')
& sortOn (\((cName, _, _, _), _, _, _, _, _, _) -> cName) & sortOn (\((cName, _, _, _), _, _, _, _, _, _) -> cName)
anyFavToggle = flip any ((,) <$> universeF <*> favouriteTerms) $ \(reason, term) ->
showFavToggle reason && not (null $ favouriteTermReason term reason)
-- We break up the default layout into two components: -- We break up the default layout into two components:
-- default-layout is the contents of the body tag, and -- default-layout is the contents of the body tag, and

View File

@ -185,7 +185,8 @@ updateFavourites cData = void . withReaderT projectBackend . runMaybeT $ do
User{userMaxFavourites} <- MaybeT $ get uid User{userMaxFavourites} <- MaybeT $ get uid
-- update Favourites -- update Favourites
for_ mcid $ \cid -> -- no need to store them with userMaxFavourites==0, since they will be removed in the pruning step anyway!
when (userMaxFavourites > 0) $ for_ mcid $ \cid ->
void . lift $ upsertBy void . lift $ upsertBy
(UniqueCourseFavourite uid cid) (UniqueCourseFavourite uid cid)
(CourseFavourite uid cid FavouriteVisited now) (CourseFavourite uid cid FavouriteVisited now)

View File

@ -4,6 +4,8 @@ module Handler.Course
import Import import Import
import qualified Database.Esqueleto as E
import qualified Database.Persist as P
import Handler.Course.Communication as Handler.Course import Handler.Course.Communication as Handler.Course
import Handler.Course.Delete as Handler.Course import Handler.Course.Delete as Handler.Course
@ -32,5 +34,35 @@ getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCNotesR = postCNotesR getCNotesR = postCNotesR
postCNotesR _ _ _ = defaultLayout [whamlet|You have corrector access to this course.|] postCNotesR _ _ _ = defaultLayout [whamlet|You have corrector access to this course.|]
-- simple redirect for now to avoid running into HTTP method not supported.
getCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler ()
getCFavouriteR tid ssh csh = redirect $ CourseR tid ssh csh CShowR
postCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler () postCFavouriteR :: TermId -> SchoolId -> CourseShorthand -> Handler ()
postCFavouriteR _ _ _ = error "not implemented" postCFavouriteR tid ssh csh = void $ do
authPair@(uid, _) <- requireAuthPair
runDB $ void $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
now <- liftIO getCurrentTime
-- should never return FavouriteCurrent
newReason <- storedFavouriteReason tid ssh csh (Just authPair) <&> (\case
-- Maybe (Maybe reason, blacklist)
Nothing -> Just FavouriteManual
Just (_reason, True) -> Just FavouriteVisited
Just (Just FavouriteManual, False) -> Nothing
Just (_reason, False) -> Just FavouriteManual)
-- change stored reason in DB
case newReason of
(Just reason) -> do
void $ E.upsertBy
(UniqueCourseFavourite uid cid)
(CourseFavourite uid cid reason now)
[P.Update CourseFavouriteReason reason P.Assign]
E.deleteBy $ UniqueCourseNoFavourite uid cid
Nothing -> do
E.deleteBy $ UniqueCourseFavourite uid cid
void $ E.upsertBy
(UniqueCourseNoFavourite uid cid)
(CourseNoFavourite uid cid)
[] -- entry shouldn't exists, but keep it unchanged anyway
-- show course page again
redirect $ CourseR tid ssh csh CShowR

View File

@ -14,6 +14,18 @@ import Handler.Utils.Invitations
import Jobs.Queue import Jobs.Queue
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
data ExamEditException
= ExamEditExamNameTaken ExamName
| ExamEditWouldBreakSheetTypeReference
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (Exception)
embedRenderMessage ''UniWorX ''ExamEditException id
getEEditR, postEEditR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html getEEditR, postEEditR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
getEEditR = postEEditR getEEditR = postEEditR
@ -27,31 +39,34 @@ postEEditR tid ssh csh examn = do
((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm course $ Just template ((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm course $ Just template
editExamAct <- formResultMaybe editExamResult $ \ExamForm{..} -> do editExamAct <- formResultMaybe editExamResult $ \ExamForm{..} -> do
insertRes <- myReplaceUnique eId Exam res <- trySql @ExamEditException $ do
{ examCourse = cid insertRes <- myReplaceUnique eId Exam
, examName = efName { examCourse = cid
, examGradingRule = efGradingRule , examName = efName
, examBonusRule = efBonusRule , examGradingRule = efGradingRule
, examOccurrenceRule = efOccurrenceRule , examBonusRule = efBonusRule
, examExamOccurrenceMapping = examExamOccurrenceMapping oldExam , examOccurrenceRule = efOccurrenceRule
, examVisibleFrom = efVisibleFrom , examExamOccurrenceMapping = examExamOccurrenceMapping oldExam
, examRegisterFrom = efRegisterFrom , examVisibleFrom = efVisibleFrom
, examRegisterTo = efRegisterTo , examRegisterFrom = efRegisterFrom
, examDeregisterUntil = efDeregisterUntil , examRegisterTo = efRegisterTo
, examPublishOccurrenceAssignments = efPublishOccurrenceAssignments , examDeregisterUntil = efDeregisterUntil
, examStart = efStart , examPublishOccurrenceAssignments = efPublishOccurrenceAssignments
, examEnd = efEnd , examStart = efStart
, examFinished = efFinished , examEnd = efEnd
, examClosed = examClosed oldExam , examFinished = efFinished
, examPublicStatistics = efPublicStatistics , examClosed = examClosed oldExam
, examGradingMode = efGradingMode , examPublicStatistics = efPublicStatistics
, examDescription = efDescription , examGradingMode = efGradingMode
, examExamMode = efExamMode , examDescription = efDescription
, examStaff = efStaff , examExamMode = efExamMode
, examPartsFrom = efPartsFrom , examStaff = efStaff
} , examPartsFrom = efPartsFrom
}
when (is _Just insertRes) $
throwM $ ExamEditExamNameTaken efName
when (is _Nothing insertRes) $ do
occIds <- fmap catMaybes . forM (Set.toList efOccurrences) $ traverse decrypt . eofId occIds <- fmap catMaybes . forM (Set.toList efOccurrences) $ traverse decrypt . eofId
deleteWhere [ ExamOccurrenceExam ==. eId, ExamOccurrenceId /<-. occIds ] deleteWhere [ ExamOccurrenceExam ==. eId, ExamOccurrenceId /<-. occIds ]
forM_ (Set.toList efOccurrences) $ \case forM_ (Set.toList efOccurrences) $ \case
@ -83,6 +98,21 @@ postEEditR tid ssh csh examn = do
} }
pIds <- fmap catMaybes . forM (Set.toList efExamParts) $ traverse decrypt . epfId pIds <- fmap catMaybes . forM (Set.toList efExamParts) $ traverse decrypt . epfId
brokenRefs <- E.selectExists . E.from $ \examPart -> do
E.where_ $ examPart E.^. ExamPartExam E.==. E.val eId
E.&&. examPart E.^. ExamPartId `E.notIn` E.valList pIds
E.where_ . E.exists . E.from $ \sheet -> do
let
sheetTypeExamPart :: E.SqlExpr (E.Value (Maybe Value))
sheetTypeExamPart = sheet E.^. SheetType E.->. "exam-part"
examPartId' :: E.SqlExpr (E.Value Value)
examPartId' = E.explicitUnsafeCoerceSqlExprValue @Value "jsonb" . E.explicitUnsafeCoerceSqlExprValue @Text "text" $ examPart E.^. ExamPartId
E.where_ $ E.maybe E.false (E.==. examPartId') sheetTypeExamPart
when brokenRefs $
throwM ExamEditWouldBreakSheetTypeReference
deleteWhere [ ExamPartExam ==. eId, ExamPartId /<-. pIds ] deleteWhere [ ExamPartExam ==. eId, ExamPartId /<-. pIds ]
forM_ (Set.toList efExamParts) $ \case forM_ (Set.toList efExamParts) $ \case
ExamPartForm{ epfId = Nothing, .. } -> insert_ ExamPartForm{ epfId = Nothing, .. } -> insert_
@ -118,9 +148,11 @@ postEEditR tid ssh csh examn = do
deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ] deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ]
sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites
return . Just $ case insertRes of return insertRes
Just _ -> addMessageI Error $ MsgExamNameTaken efName
Nothing -> do return . Just $ case res of
Left exc -> addMessageI Error exc
Right _ -> do
addMessageI Success $ MsgExamEdited efName addMessageI Success $ MsgExamEdited efName
redirect $ CExamR tid ssh csh efName EShowR redirect $ CExamR tid ssh csh efName EShowR

View File

@ -98,7 +98,7 @@ postCExamNewR tid ssh csh = do
runConduit $ selectSource [ CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantInactive True ] [] .| C.mapM_ recordNoShow runConduit $ selectSource [ CourseParticipantCourse ==. cid, CourseParticipantState ==. CourseParticipantInactive True ] [] .| C.mapM_ recordNoShow
return . Just $ case insertRes of return . Just $ case insertRes of
Nothing -> addMessageI Error $ MsgExamNameTaken efName Nothing -> addMessageI Error $ MsgExamEditExamNameTaken efName
Just _ -> do Just _ -> do
addMessageI Success $ MsgExamCreated efName addMessageI Success $ MsgExamCreated efName
redirect $ CourseR tid ssh csh CExamListR redirect $ CourseR tid ssh csh CExamListR

View File

@ -14,7 +14,10 @@ import qualified Database.Esqueleto.Utils as E
import Development.GitRev import Development.GitRev
import Auth.LDAP (ADError(..), ADInvalidCredentials(..)) import Auth.LDAP (ADError(..), ADInvalidCredentials(..), CampusMessage(..))
import Yesod.Auth.Message(AuthMessage(..))
-- | Versionsgeschichte -- | Versionsgeschichte
getVersionR :: Handler TypedContent getVersionR :: Handler TypedContent

View File

@ -54,8 +54,6 @@ $(deriveSimpleWith ''ToMessage 'toMessage (over Text.packed $ Text.intercalate "
derivePersistField "Theme" derivePersistField "Theme"
data FavouriteReason data FavouriteReason
= FavouriteVisited = FavouriteVisited
| FavouriteParticipant | FavouriteParticipant
@ -68,6 +66,9 @@ deriveJSON defaultOptions
} ''FavouriteReason } ''FavouriteReason
derivePersistFieldJSON ''FavouriteReason derivePersistFieldJSON ''FavouriteReason
makePrisms ''FavouriteReason
data Sex data Sex
= SexNotKnown = SexNotKnown
| SexMale | SexMale

View File

@ -198,6 +198,7 @@ data FormIdentifier
| FIDmaterial | FIDmaterial
| FIDCourseNews | FIDCourseNews
| FIDCourseEvent | FIDCourseEvent
| FIDCourseFavouriteToggle
| FIDsubmission | FIDsubmission
| FIDsettings | FIDsettings
| FIDcorrectors | FIDcorrectors

View File

@ -37,6 +37,7 @@ data Icon
| IconVisible | IconVisible
| IconInvisible | IconInvisible
| IconCourse | IconCourse
| IconCourseFavouriteManual | IconCourseFavouriteAutomatic | IconCourseFavouriteOff
| IconEnrolTrue | IconEnrolTrue
| IconEnrolFalse | IconEnrolFalse
| IconPlanned | IconPlanned
@ -110,6 +111,9 @@ iconText = \case
IconVisible -> "eye" IconVisible -> "eye"
IconInvisible -> "eye-slash" IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap" IconCourse -> "graduation-cap"
IconCourseFavouriteManual -> "star"
IconCourseFavouriteAutomatic -> "star-half-alt"
IconCourseFavouriteOff -> "slash" -- TODO use FA regular style star for stacked icon
IconEnrolTrue -> "user-plus" IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash" IconEnrolFalse -> "user-slash"
IconPlanned -> "cog" IconPlanned -> "cog"
@ -189,6 +193,23 @@ icon ic = [shamlet|
$newline never $newline never
<i .fas .fa-#{iconText ic}> <i .fas .fa-#{iconText ic}>
|] |]
-- Create an icon from font-awesome with fixed width
iconFixed :: Icon -> Markup
iconFixed ic = [shamlet|
$newline never
<i .fas .fa-fw .fa-#{iconText ic}>
|]
-- Stack two icons from font-awesome without additional space
iconStacked :: Icon -> Icon -> Markup
iconStacked ic0 ic1
= [shamlet|
$newline never
<span .fa-stack .icon--stacked>
<i .fas .fa-stack-2x .fa-#{iconText ic0}>
<i .fas .fa-stack-2x .fa-#{iconText ic1}>
|]
-- Create an icon (defaults to "?") with a specified tooltip -- Create an icon (defaults to "?") with a specified tooltip
iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site () iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site ()

View File

@ -5,6 +5,7 @@ module Utils.Sql
, catchSql, handleSql , catchSql, handleSql
, isUniqueConstraintViolation , isUniqueConstraintViolation
, catchIfSql, handleIfSql , catchIfSql, handleIfSql
, trySql
) where ) where
import ClassyPrelude.Yesod hiding (handle) import ClassyPrelude.Yesod hiding (handle)
@ -125,5 +126,8 @@ catchIfSql p = flip $ handleIfSql p
handleIfSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => (e -> Bool) -> (e -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a handleIfSql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => (e -> Bool) -> (e -> SqlPersistT m a) -> SqlPersistT m a -> SqlPersistT m a
handleIfSql p recover = handleSql (\err -> bool throwM recover (p err) err) handleIfSql p recover = handleSql (\err -> bool throwM recover (p err) err)
trySql :: forall e m a. (MonadCatch m, MonadIO m, Exception e) => SqlPersistT m a -> SqlPersistT m (Either e a)
trySql = handleSql (return . Left) . fmap Right
isUniqueConstraintViolation :: SqlError -> Bool isUniqueConstraintViolation :: SqlError -> Bool
isUniqueConstraintViolation SqlError{..} = "duplicate key value violates unique constraint" `ByteString.isPrefixOf` sqlErrorMsg isUniqueConstraintViolation SqlError{..} = "duplicate key value violates unique constraint" `ByteString.isPrefixOf` sqlErrorMsg

View File

@ -4,32 +4,66 @@ $newline never
Können sie sich mit <i>exakt identischen</i> (idealerweise # Können sie sich mit <i>exakt identischen</i> (idealerweise #
copy&paste) Daten # copy&paste) Daten #
im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> # im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
anmelden?<br /> anmelden?
Falls nicht, ist davon auszugehen, dass Sie Ihre Anmeldedaten falsch # <br>
eingeben oder <a href=^{faqLink FAQNoCampusAccount}>keine #
LMU-Benutzerkennung (ehem. Campus-Kennung) besitzen</a>. Falls nicht („_{InvalidLogin}“), ist davon auszugehen, dass Sie #
Ihre Anmeldedaten falsch eingeben oder #
<a href=^{faqLink FAQNoCampusAccount}>keine LMU-Benutzerkennung #
(ehem. Campus-Kennung) besitzen</a>.
<p> <p>
Beachten Sie dabei auch, dass Uni2work Leerzeichen sowohl im # Beachten Sie dabei auch, dass Uni2work Leerzeichen sowohl im #
Passwort als auch bei der Kennung berücksichtigt.<br /> Passwort als auch bei der Kennung berücksichtigt.
<br>
Beim Passwort ist zudem Groß- und Kleinschreibung relevant. Beim Passwort ist zudem Groß- und Kleinschreibung relevant.
<br>
Prüfen Sie bitte (auch wenn Sie einen Passwortmanager verwenden) mit #
dem Auge-Symbol rechts neben dem Passwort-Feld (nur verfügbar mit #
aktiviertem JavaScript), dass Sie Ihr Passwort korrekt eingeben.
<p> <p>
Uni2work bietet zwei Login-Formulare. Uni2work bietet zwei Login-Formulare.
<br> <br>
Für die Anmeldung mit der LMU-Benutzerkennung (ehem. Campus-Kennung) # Für die Anmeldung mit der LMU-Benutzerkennung (ehem. Campus-Kennung) #
müssen Sie das Formular „Campus-Login“ verwenden. müssen Sie das Formular „_{MsgLDAPLoginTitle}“ verwenden.
<br> <br>
Geben Sie unter „Campus-Kennung“ ihre vollständige #
LMU-Benutzerkennung an. Geben Sie unter „_{MsgCampusIdent}“ ihre vollständige #
LMU-Benutzerkennung an. #
Diese ist identisch mit ihrer <code>@campus.lmu.de</code> E-Mail # Diese ist identisch mit ihrer <code>@campus.lmu.de</code> E-Mail #
Adresse. Adresse.
<p> <p>
Falls Sie sich # Falls Sie seit Ihrem letzten Login in Uni2work ihr Passwort geändert #
im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> # haben, kann es sein, dass die Änderung des Passworts (noch) nicht #
anmelden können, aber nicht in Uni2work, wenden Sie sich bitte über # korrekt propagiert wurde.
das <a href=@{HelpR}>Hilfe-Formular</a>, oben rechts auf jeder #
Seite, an die Uni2work-Administration. <br>
In diesem Fall können Sie versuchen Ihr Passwort erneut zu ändern.
<p>
Sobald Sie die obigen Hinweise befolgt haben, wenden Sie sich bitte #
(erneut) über das <a href=@{HelpR}>Hilfe-Formular</a>, oben rechts #
auf jeder Seite, an die Uni2work-Administration.
<br>
Erwähnen Sie dabei bitte, dass Sie die in diesem FAQ-Punkt #
aufgeführten Hinweise beachtet haben und schildern Sie, welche #
Schritte Sie bereits ergriffen haben.
<br>
Teilen Sie Ihr Passwort niemals mit Dritten (auch nicht der #
Uni2work-Administration oder dem IT-Servicedesk der LMU)!

View File

@ -4,31 +4,68 @@ $newline never
Can you log in to # Can you log in to #
the <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> # the <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
using the <i>exact same</i> (ideally copied & pasted) login data? using the <i>exact same</i> (ideally copied & pasted) login data?
<br> <br>
If you cannot (“Invalid Login”), this means that you are entering # If you cannot (“_{InvalidLogin}”), this means that you are #
your login data wrong or that you # entering your login data wrong or that you #
<a href=^{faqLink FAQNoCampusAccount}>do not have a LMU user ID # <a href=^{faqLink FAQNoCampusAccount}>do not have a LMU user ID #
(formerly Campus-ID)</a>. (formerly Campus-ID)</a>.
<p> <p>
Please consider that for Uni2work both your user ID and password are # Please consider that for Uni2work both your user ID and password are #
sensitive to whitespace characters. sensitive to whitespace characters.
<br> <br>
Your password is also case-sensitive. Your password is also case-sensitive.
<br>
Please also use the eye-symbol next to the password field to check #
that you have entered your password correctly. #
The eye-symbol is only visible if JavaScript is activated in your #
browser. #
Please check your password in this way, even if you are using a #
password manager instead of typing it manually.
<p> <p>
Uni2work offers two login forms. Uni2work offers two login forms.
<br> <br>
To log in using your LMU user ID (formerly Campus-ID) you need to # To log in using your LMU user ID (formerly Campus-ID) you need to #
use the form titled “Campus login”. use the form titled “_{MsgLDAPLoginTitle}”.
<br> <br>
Under “Campus account” please enter either your entire LMU user ID, #
which is identical to your <code>@campus.lmu.de</code> email address. Under “_{MsgCampusIdent}” please enter your entire LMU user ID, #
which is identical to your <code>@campus.lmu.de</code> email #
address.
<p> <p>
If you can log in to # If you have changed your password since last you logged into #
the <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> # Uni2work, it may be the case that your password change was not #
but can't log in to Uni2work, please contact a # propagated properly.
Uni2work-Administrator using the <a href=@{HelpR}>Support form</a> #
<br>
If so, please try changing your password again.
<p>
Once you have followed the suggestions above, please contact a #
Uni2work-administrator using the <a href=@{HelpR}>Support form</a> #
(at the top right of every page). (at the top right of every page).
<br>
Please include that you have read this faq-entry and which steps you #
have already taken.
<br>
Never disclose your password to third parties! #
Not even to an Uni2work-administrator or the IT-Servicedesk!

View File

@ -9,35 +9,46 @@ $newline never
_{MsgLogo} _{MsgLogo}
<div .asidenav> <div .asidenav>
$forall tid <- favouriteTerms $if null favouriteTerms && is _Just muid
<div .asidenav__box> <div .asidenav__box-explanation>
<h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right> _{MsgFavouritesEmptyTip}
<div .asidenav-term-identifier--long> $else
_{ShortTermIdentifier tid} $forall tid <- favouriteTerms
<div .asidenav-term-identifier--short> <div .asidenav__box>
#{toPathPiece tid} <h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right>
$forall favReason <- sortOn Down universeF <div .asidenav-term-identifier--long>
$if not (null $ favouriteTermReason tid favReason) _{ShortTermIdentifier tid}
<h3 .asidenav__box-subtitle> <div .asidenav-term-identifier--short>
_{favReason} #{toPathPiece tid}
<ul .asidenav__list.list--iconless> $forall favReason <- sortOn Down universeF
$forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason $if not (null $ favouriteTermReason tid favReason)
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active> <h3 .asidenav__box-subtitle>
<a .asidenav__link-wrapper href=@{courseRoute}> _{favReason}
<div .asidenav__link-shorthand>#{csh} <ul .asidenav__list.list--iconless>
<div .asidenav__link-label> $forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason
#{cName} <li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
$if mayEdit && not courseVisible <a .asidenav__link-wrapper href=@{courseRoute}>
\ #{iconInvisible} <div .asidenav__link-shorthand>#{csh}
<div .asidenav__nested-list-wrapper> <div .asidenav__link-label>
$maybe pageActions <- mPageActions <div .asidenav__link-label-text>
<ul .asidenav__nested-list.list--iconless> #{cName}
$forall (label, route) <- pageActions $if mayEdit && not courseVisible
<li .asidenav__nested-list-item> \ #{iconInvisible}
<a .asidenav__link-wrapper href=#{route}>#{label} $if showFavToggle favReason
$nothing <div .asidenav__link-favourite-toggle>
<p .asidenav__nested-list--unavailable> ^{favouriteToggleWgt}
_{MsgFavouritesUnavailableTip} <div .asidenav__nested-list-wrapper>
$maybe pageActions <- mPageActions
<ul .asidenav__nested-list.list--iconless>
$forall (label, route) <- pageActions
<li .asidenav__nested-list-item>
<a .asidenav__link-wrapper href=#{route}>#{label}
$nothing
<p .asidenav__nested-list--unavailable>
_{MsgFavouritesUnavailableTip}
$if anyFavToggle
<div .asidenav__box-explanation>
_{MsgFavouritesToggleTip}
<div .asidenav__sigillum> <div .asidenav__sigillum>
<img src=@{StaticR img_lmu_sigillum_svg}> <img src=@{StaticR img_lmu_sigillum_svg}>