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: length xs < n, 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.
## [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.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"
user-defaults:
max-favourites: 12
max-favourites: 0
max-favourite-terms: 2
theme: Default
date-time-format: "%a %d %b %Y %R"

View File

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

View File

@ -1,3 +1,6 @@
@use "../../common" as *
@use "../../app" as *
.main__aside
position: fixed
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3)
@ -90,6 +93,18 @@
padding: 0 13px
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
.asidenav__logo
@ -217,9 +232,30 @@
.asidenav__link-shorthand
display: none
.asidenav__link-favourite-toggle
opacity: .33
&:hover
opacity: 1
button
display: flex
text-decoration: none
.asidenav__link-label
display: flex
justify-content: space-between
align-items: center
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
.asidenav__nested-list-wrapper
position: absolute

View File

@ -83,7 +83,6 @@ CosubmittorTip: Einladungen per E-Mail erhalten genau jene Adressen, für die ni
CorrectionsTitle: Zugewiesene Korrekturen
CorrectorsHead sheetName@SheetName: Korrektoren für #{sheetName}
<<<<<<< Updated upstream
MaterialName: Name
MaterialType: Art
MaterialTypePlaceholder: Folien, Code, Beispiel, ...
@ -180,8 +179,6 @@ MaterialFree: Kursmaterialien ohne Anmeldung zugänglich
UnauthorizedWrite: Sie haben hierfür keine Schreibberechtigung
UnauthorizedSystemMessageTime: Diese Systemnachricht ist noch nicht oder nicht mehr 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}“
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)}%)
TableProportionNoRatio c@Text of'@Text: #{c}/#{of'}
<<<<<<< Updated upstream
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
=======
>>>>>>> Stashed changes
CsvColumnUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt
CsvColumnUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
@ -581,8 +575,13 @@ FavouriteParticipant: Ihre Kurse
FavouriteManual: Favoriten
FavouriteCurrent: Aktueller Kurs
<<<<<<< HEAD
=======
>>>>>>> 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.
@ -669,3 +668,15 @@ UrlFieldCouldNotParseAbsolute: Konnte nicht als absolute URL interpretiert werde
WGFTextInput: Textfeld
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
CorrectorsHead sheetName: Correctors for #{sheetName}
<<<<<<< Updated upstream
MaterialName: Name
MaterialType: Type
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
UnauthorizedSystemMessageTime: This system-message is not currently available.
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}”
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)}%)
TableProportionNoRatio c of': #{c}/#{of'}
<<<<<<< Updated upstream
ParticipantsCsvName 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
CsvColumnUserDegree: Degree the participant pursues in 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
FavouriteCurrent: Current course
<<<<<<< HEAD
=======
>>>>>>> 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.
UserSimplifiedFeaturesOfStudyCsv: Simplified features of study

2
package-lock.json generated
View File

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

View File

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

View File

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

2
routes
View File

@ -174,7 +174,7 @@
!/course/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ 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-template CRegisterTemplateR GET !course-time
/edit CEditR GET POST

View File

@ -32,6 +32,7 @@ module Database.Esqueleto.Utils
, selectMaybe
, day, diffDays, diffTimes
, exprLift
, explicitUnsafeCoerceSqlExprValue
, module Database.Esqueleto.Utils.TH
) where
@ -55,6 +56,8 @@ import Data.Coerce (Coercible)
import Data.Time.Clock (NominalDiffTime)
import qualified Data.Text.Lazy.Builder as Text.Builder
{-# ANN any ("HLint: ignore Use any" :: 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)
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.ERaw p f) = E.ERaw E.Parens $ \info ->
let (b1, vals) = f info

View File

@ -10,7 +10,6 @@ module Foundation.Instances
import Import.NoFoundation
import qualified Data.Text as Text
import qualified Data.List as List
import Data.List (inits)
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.Auth as UniWorX
import Foundation.Instances.ButtonClass
import Foundation.SiteLayout
import Foundation.Type
import Foundation.I18n
@ -51,29 +51,6 @@ import qualified Data.CaseInsensitive as CI
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
-- of settings which can be configured by overriding methods here.
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
, siteLayoutMsg', siteLayoutMsg
, getSystemMessageState
, storedFavouriteReason
) where
import Import.NoFoundation hiding (embedFile, runDB)
@ -15,6 +16,7 @@ import Foundation.Routes
import Foundation.Navigation
import Foundation.I18n
import Foundation.Yesod.Persist
import Foundation.Instances.ButtonClass
import Utils.SystemMessage
import Utils.Form
@ -37,6 +39,61 @@ import Text.Cassius (cassiusFile)
import Text.Hamlet (hamletFile)
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
= MemcachedKeyFavouriteQuickActions (TermId, SchoolId, CourseShorthand) AuthContext (NonEmpty Lang)
@ -54,16 +111,15 @@ data MemcachedLimitKeyFavourites
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
{-# 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
siteLayout :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend
, Button UniWorX ButtonSubmit
)
=> WidgetFor UniWorX () -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -71,7 +127,6 @@ siteLayout = siteLayout' . Just
siteLayout' :: ( BearerAuthSite UniWorX
, YesodPersistBackend UniWorX ~ SqlBackend
, Button UniWorX ButtonSubmit
)
=> Maybe (WidgetFor UniWorX ()) -- ^ `pageHeading`
-> WidgetFor UniWorX () -> HandlerFor UniWorX Html
@ -94,11 +149,11 @@ siteLayout' overrideHeading widget = do
now <- liftIO getCurrentTime
muid <- maybeAuthPair
-- Lookup Favourites, Breadcrumbs, Headline, & Theme if possible
(favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme) <- do
muid <- maybeAuthPair
(favourites', (title, parents), nav', contentHeadline, mmsgs, maxFavouriteTerms, currentTheme, storedReasonAndToggleRoute) <- do
(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
E.on $ E.just (course E.^. CourseId) E.==. courseFavourite E.?. CourseFavouriteCourse
E.&&. courseFavourite E.?. CourseFavouriteUser E.==. E.val (view _1 <$> muid)
@ -189,8 +244,12 @@ siteLayout' overrideHeading widget = do
forM_ authTagPivots $
\authTag -> addMessageWidget Info $ msgModal [whamlet|_{MsgUnauthorizedDisabledTag authTag}|] (Left $ SomeRoute (AuthPredsR, catMaybes [(toPathPiece GetReferer, ) . toPathPiece <$> mcurrentRoute]))
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
, breadcrumbs''
@ -199,10 +258,37 @@ siteLayout' overrideHeading widget = do
, mmsgs
, maybe userDefaultMaxFavouriteTerms userMaxFavouriteTerms $ 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]
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)
-> let courseRoute = CourseR tid ssh csh CShowR
@ -251,6 +337,8 @@ siteLayout' overrideHeading widget = do
favouriteTermReason tid favReason' = favourites
& filter (\((_, tid', _, _), _, _, favReason, _, _, _) -> unTermKey tid' == tid && favReason == favReason')
& 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:
-- 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
-- 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
(UniqueCourseFavourite uid cid)
(CourseFavourite uid cid FavouriteVisited now)

View File

@ -4,6 +4,8 @@ module Handler.Course
import Import
import qualified Database.Esqueleto as E
import qualified Database.Persist as P
import Handler.Course.Communication as Handler.Course
import Handler.Course.Delete as Handler.Course
@ -32,5 +34,35 @@ getCNotesR, postCNotesR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCNotesR = postCNotesR
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 _ _ _ = 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 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
@ -27,31 +39,34 @@ postEEditR tid ssh csh examn = do
((editExamResult, editExamWidget), editExamEnctype) <- runFormPost . validateForm (validateExam cid $ Just exam) . examForm course $ Just template
editExamAct <- formResultMaybe editExamResult $ \ExamForm{..} -> do
insertRes <- myReplaceUnique eId Exam
{ examCourse = cid
, examName = efName
, examGradingRule = efGradingRule
, examBonusRule = efBonusRule
, examOccurrenceRule = efOccurrenceRule
, examExamOccurrenceMapping = examExamOccurrenceMapping oldExam
, examVisibleFrom = efVisibleFrom
, examRegisterFrom = efRegisterFrom
, examRegisterTo = efRegisterTo
, examDeregisterUntil = efDeregisterUntil
, examPublishOccurrenceAssignments = efPublishOccurrenceAssignments
, examStart = efStart
, examEnd = efEnd
, examFinished = efFinished
, examClosed = examClosed oldExam
, examPublicStatistics = efPublicStatistics
, examGradingMode = efGradingMode
, examDescription = efDescription
, examExamMode = efExamMode
, examStaff = efStaff
, examPartsFrom = efPartsFrom
}
res <- trySql @ExamEditException $ do
insertRes <- myReplaceUnique eId Exam
{ examCourse = cid
, examName = efName
, examGradingRule = efGradingRule
, examBonusRule = efBonusRule
, examOccurrenceRule = efOccurrenceRule
, examExamOccurrenceMapping = examExamOccurrenceMapping oldExam
, examVisibleFrom = efVisibleFrom
, examRegisterFrom = efRegisterFrom
, examRegisterTo = efRegisterTo
, examDeregisterUntil = efDeregisterUntil
, examPublishOccurrenceAssignments = efPublishOccurrenceAssignments
, examStart = efStart
, examEnd = efEnd
, examFinished = efFinished
, examClosed = examClosed oldExam
, examPublicStatistics = efPublicStatistics
, examGradingMode = efGradingMode
, examDescription = efDescription
, examExamMode = efExamMode
, 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
deleteWhere [ ExamOccurrenceExam ==. eId, ExamOccurrenceId /<-. occIds ]
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
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 ]
forM_ (Set.toList efExamParts) $ \case
ExamPartForm{ epfId = Nothing, .. } -> insert_
@ -118,9 +148,11 @@ postEEditR tid ssh csh examn = do
deleteWhere [ InvitationFor ==. invRef @ExamCorrector eId, InvitationEmail /<-. invites ]
sinkInvitationsF examCorrectorInvitationConfig $ map (, eId, (InvDBDataExamCorrector, InvTokenDataExamCorrector)) invites
return . Just $ case insertRes of
Just _ -> addMessageI Error $ MsgExamNameTaken efName
Nothing -> do
return insertRes
return . Just $ case res of
Left exc -> addMessageI Error exc
Right _ -> do
addMessageI Success $ MsgExamEdited efName
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
return . Just $ case insertRes of
Nothing -> addMessageI Error $ MsgExamNameTaken efName
Nothing -> addMessageI Error $ MsgExamEditExamNameTaken efName
Just _ -> do
addMessageI Success $ MsgExamCreated efName
redirect $ CourseR tid ssh csh CExamListR

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ data Icon
| IconVisible
| IconInvisible
| IconCourse
| IconCourseFavouriteManual | IconCourseFavouriteAutomatic | IconCourseFavouriteOff
| IconEnrolTrue
| IconEnrolFalse
| IconPlanned
@ -110,6 +111,9 @@ iconText = \case
IconVisible -> "eye"
IconInvisible -> "eye-slash"
IconCourse -> "graduation-cap"
IconCourseFavouriteManual -> "star"
IconCourseFavouriteAutomatic -> "star-half-alt"
IconCourseFavouriteOff -> "slash" -- TODO use FA regular style star for stacked icon
IconEnrolTrue -> "user-plus"
IconEnrolFalse -> "user-slash"
IconPlanned -> "cog"
@ -189,6 +193,23 @@ icon ic = [shamlet|
$newline never
<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
iconTooltip :: forall site. WidgetFor site () -> Maybe Icon -> Bool -> WidgetFor site ()

View File

@ -5,6 +5,7 @@ module Utils.Sql
, catchSql, handleSql
, isUniqueConstraintViolation
, catchIfSql, handleIfSql
, trySql
) where
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 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{..} = "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 #
copy&paste) Daten #
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 #
eingeben oder <a href=^{faqLink FAQNoCampusAccount}>keine #
LMU-Benutzerkennung (ehem. Campus-Kennung) besitzen</a>.
<br>
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>
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.
<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>
Uni2work bietet zwei Login-Formulare.
<br>
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>
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 #
Adresse.
<p>
Falls Sie sich #
im <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
anmelden können, aber nicht in Uni2work, wenden Sie sich bitte über #
das <a href=@{HelpR}>Hilfe-Formular</a>, oben rechts auf jeder #
Seite, an die Uni2work-Administration.
Falls Sie seit Ihrem letzten Login in Uni2work ihr Passwort geändert #
haben, kann es sein, dass die Änderung des Passworts (noch) nicht #
korrekt propagiert wurde.
<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 #
the <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
using the <i>exact same</i> (ideally copied & pasted) login data?
<br>
If you cannot (“Invalid Login”), this means that you are entering #
your login data wrong or that you #
If you cannot (“_{InvalidLogin}”), this means that you are #
entering your login data wrong or that you #
<a href=^{faqLink FAQNoCampusAccount}>do not have a LMU user ID #
(formerly Campus-ID)</a>.
<p>
Please consider that for Uni2work both your user ID and password are #
sensitive to whitespace characters.
<br>
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>
Uni2work offers two login forms.
<br>
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>
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>
If you can log in to #
the <a href="https://www.portal.uni-muenchen.de">Campus-Portal</a> #
but can't log in to Uni2work, please contact a #
Uni2work-Administrator using the <a href=@{HelpR}>Support form</a> #
If you have changed your password since last you logged into #
Uni2work, it may be the case that your password change was not #
propagated properly.
<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).
<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}
<div .asidenav>
$forall tid <- favouriteTerms
<div .asidenav__box>
<h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right>
<div .asidenav-term-identifier--long>
_{ShortTermIdentifier tid}
<div .asidenav-term-identifier--short>
#{toPathPiece tid}
$forall favReason <- sortOn Down universeF
$if not (null $ favouriteTermReason tid favReason)
<h3 .asidenav__box-subtitle>
_{favReason}
<ul .asidenav__list.list--iconless>
$forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{csh}
<div .asidenav__link-label>
#{cName}
$if mayEdit && not courseVisible
\ #{iconInvisible}
<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 null favouriteTerms && is _Just muid
<div .asidenav__box-explanation>
_{MsgFavouritesEmptyTip}
$else
$forall tid <- favouriteTerms
<div .asidenav__box>
<h3 .asidenav__box-title uw-show-hide data-show-hide-id="#{termToText tid}" data-show-hide-align=right>
<div .asidenav-term-identifier--long>
_{ShortTermIdentifier tid}
<div .asidenav-term-identifier--short>
#{toPathPiece tid}
$forall favReason <- sortOn Down universeF
$if not (null $ favouriteTermReason tid favReason)
<h3 .asidenav__box-subtitle>
_{favReason}
<ul .asidenav__list.list--iconless>
$forall ((cName, _, _, csh), courseRoute, mPageActions, _, courseVisible, _, mayEdit) <- favouriteTermReason tid favReason
<li .asidenav__list-item :highlight courseRoute:.asidenav__list-item--active>
<a .asidenav__link-wrapper href=@{courseRoute}>
<div .asidenav__link-shorthand>#{csh}
<div .asidenav__link-label>
<div .asidenav__link-label-text>
#{cName}
$if mayEdit && not courseVisible
\ #{iconInvisible}
$if showFavToggle favReason
<div .asidenav__link-favourite-toggle>
^{favouriteToggleWgt}
<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>
<img src=@{StaticR img_lmu_sigillum_svg}>