Merge branch 'master' into course-teaser
This commit is contained in:
commit
7404b7b63b
1
.gitignore
vendored
1
.gitignore
vendored
@ -35,3 +35,4 @@ src/Handler/Course.SnapCustom.hs
|
|||||||
tags
|
tags
|
||||||
test.log
|
test.log
|
||||||
*.dump-splices
|
*.dump-splices
|
||||||
|
/.stack-work.lock
|
||||||
|
|||||||
47
CHANGELOG.md
47
CHANGELOG.md
@ -2,6 +2,53 @@
|
|||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
### [2.1.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.1.0...v2.1.1) (2019-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **assign correctors:** also show names of unenlisted correctors ([de49a77](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/de49a77))
|
||||||
|
* **build:** fix build ([49dc413](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/49dc413))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.1.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v2.0.0...v2.1.0) (2019-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **corrector handling:** show correctors by a consistent order ([9c5ed5f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9c5ed5f))
|
||||||
|
* **translation:** fix typos in translations; add bug to known bugs ([ac3f7bb](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ac3f7bb))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **csv:** introduce csv export ([631bbef](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/631bbef))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v1.4.1...v2.0.0) (2019-07-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **correction:** comment column made wide in online correction form ([d83b1f6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d83b1f6)), closes [#373](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/373)
|
||||||
|
* **number-input-fields:** number inputs made HTML5 compatible ([6098215](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/6098215)), closes [#412](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/issues/412)
|
||||||
|
* **ratings:** disallow ratings for graded sheets without point value ([c0b90c4](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c0b90c4))
|
||||||
|
* **tooltips:** fixes font-color when used in tableheaders ([f4bb70e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f4bb70e))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **exams:** show study features of registered users ([04bea76](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/04bea76))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* **exams:** E.isInfixOf and E.hasInfix
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### [1.4.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v1.4.0...v1.4.1) (2019-07-04)
|
### [1.4.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v1.4.0...v1.4.1) (2019-07-04)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
build.sh
2
build.sh
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
echo "Building..."
|
echo "Building..."
|
||||||
stack build --fast --flag uniworx:-library-only --flag uniworx:dev $@
|
stack build --fast --flag uniworx:-library-only --flag uniworx:dev $@
|
||||||
echo "Done."
|
echo "Done."
|
||||||
|
|||||||
4
clean.sh
4
clean.sh
@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
case $1 in
|
case $1 in
|
||||||
"")
|
"")
|
||||||
exec -- stack clean
|
exec -- stack clean
|
||||||
|
|||||||
3
db.sh
3
db.sh
@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Options: see /test/Database.hs (Main)
|
# Options: see /test/Database.hs (Main)
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
stack build --fast --flag uniworx:-library-only --flag uniworx:dev
|
||||||
stack exec uniworxdb -- $@
|
stack exec uniworxdb -- $@
|
||||||
|
|||||||
21
deploy.sh
21
deploy.sh
@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
configFile=""
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
test)
|
|
||||||
ln -svf "keter_testworx.yml" config/keter.yml
|
|
||||||
|
|
||||||
yesod keter
|
|
||||||
;;
|
|
||||||
production)
|
|
||||||
ln -svf "keter_uni2work.yml" config/keter.yml
|
|
||||||
|
|
||||||
yesod keter && git tag -f live && git push origin live
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 (test|production)" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
6
frontend/vendor/fontawesome.css
vendored
6
frontend/vendor/fontawesome.css
vendored
File diff suppressed because one or more lines are too long
4
ghci.sh
4
ghci.sh
@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
unset HOST
|
unset HOST
|
||||||
export DETAILED_LOGGING=true
|
export DETAILED_LOGGING=true
|
||||||
export LOG_ALL=true
|
export LOG_ALL=true
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
move-back() {
|
move-back() {
|
||||||
mv -v .stack-work .stack-work-doc
|
mv -v .stack-work .stack-work-doc
|
||||||
[[ -d .stack-work-build ]] && mv -v .stack-work-build .stack-work
|
[[ -d .stack-work-build ]] && mv -v .stack-work-build .stack-work
|
||||||
|
|||||||
4
hlint.sh
4
hlint.sh
@ -1,3 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
exec -- stack build --test --fast --flag uniworx:dev --flag uniworx:library-only uniworx:test:hlint
|
exec -- stack build --test --fast --flag uniworx:dev --flag uniworx:library-only uniworx:test:hlint
|
||||||
|
|||||||
@ -705,7 +705,7 @@ NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
|
|||||||
NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben
|
NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben
|
||||||
NotificationTriggerSheetInactive: Abgabezeitraum eines meiner Übungsblätter ist abgelaufen
|
NotificationTriggerSheetInactive: Abgabezeitraum eines meiner Übungsblätter ist abgelaufen
|
||||||
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt
|
||||||
NotificationTriggerCorrectionsNotDistributed: Abgaben eines meiner Übungsblätter konnten keinem Korrektur zugeteilt werden
|
NotificationTriggerCorrectionsNotDistributed: Nicht alle Abgaben eines meiner Übungsblätter konnten einem Korrektor zugeteilt werden
|
||||||
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
|
||||||
|
|
||||||
CorrCreate: Abgaben erstellen
|
CorrCreate: Abgaben erstellen
|
||||||
@ -1171,3 +1171,10 @@ VersionHistory: Versionsgeschichte
|
|||||||
KnownBugs: Bekannte Bugs
|
KnownBugs: Bekannte Bugs
|
||||||
|
|
||||||
ExamUsersHeading: Klausurteilnehmer
|
ExamUsersHeading: Klausurteilnehmer
|
||||||
|
|
||||||
|
CsvFile: CSV-Datei
|
||||||
|
CsvModifyExisting: Existierende Einträge angleichen
|
||||||
|
CsvAddNew: Neue Einträge einfügen
|
||||||
|
CsvDeleteMissing: Fehlende Einträge entfernen
|
||||||
|
BtnCsvExport: CSV-Datei exportieren
|
||||||
|
BtnCsvImport: CSV-Datei importieren
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "1.4.1",
|
"version": "2.1.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "uni2work",
|
"name": "uni2work",
|
||||||
"version": "1.4.1",
|
"version": "2.1.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -9,10 +9,12 @@
|
|||||||
"start": "run-p frontend:build:watch yesod:start",
|
"start": "run-p frontend:build:watch yesod:start",
|
||||||
"test": "run-s frontend:test yesod:test",
|
"test": "run-s frontend:test yesod:test",
|
||||||
"lint": "run-s frontend:lint yesod:lint",
|
"lint": "run-s frontend:lint yesod:lint",
|
||||||
|
"build": "run-s frontend:build yesod:build",
|
||||||
"yesod:db": "./db.sh",
|
"yesod:db": "./db.sh",
|
||||||
"yesod:start": "./start.sh",
|
"yesod:start": "./start.sh",
|
||||||
"yesod:lint": "./hlint.sh",
|
"yesod:lint": "./hlint.sh",
|
||||||
"yesod:test": "./test.sh",
|
"yesod:test": "./test.sh",
|
||||||
|
"yesod:build": "./build.sh",
|
||||||
"frontend:lint": "eslint frontend/src",
|
"frontend:lint": "eslint frontend/src",
|
||||||
"frontend:test": "karma start --conf karma.conf.js",
|
"frontend:test": "karma start --conf karma.conf.js",
|
||||||
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false",
|
"frontend:test:watch": "karma start --conf karma.conf.js --single-run false",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
name: uniworx
|
name: uniworx
|
||||||
version: 1.4.1
|
version: 2.1.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
# Due to a bug in GHC 8.0.1, we block its usage
|
# Due to a bug in GHC 8.0.1, we block its usage
|
||||||
@ -32,6 +32,7 @@ dependencies:
|
|||||||
- data-default
|
- data-default
|
||||||
- aeson >=0.6 && <1.3
|
- aeson >=0.6 && <1.3
|
||||||
- conduit >=1.0 && <2.0
|
- conduit >=1.0 && <2.0
|
||||||
|
- conduit-combinators
|
||||||
- monad-logger >=0.3 && <0.4
|
- monad-logger >=0.3 && <0.4
|
||||||
- fast-logger >=2.2 && <2.5
|
- fast-logger >=2.2 && <2.5
|
||||||
- wai-logger >=2.2 && <2.4
|
- wai-logger >=2.2 && <2.4
|
||||||
@ -129,6 +130,9 @@ dependencies:
|
|||||||
- hourglass
|
- hourglass
|
||||||
- unix
|
- unix
|
||||||
- stm-delay
|
- stm-delay
|
||||||
|
- cassava
|
||||||
|
- cassava-conduit
|
||||||
|
- constraints
|
||||||
|
|
||||||
other-extensions:
|
other-extensions:
|
||||||
- GeneralizedNewtypeDeriving
|
- GeneralizedNewtypeDeriving
|
||||||
|
|||||||
@ -7,7 +7,7 @@ module Database.Esqueleto.Utils
|
|||||||
, any, all
|
, any, all
|
||||||
, SqlIn(..)
|
, SqlIn(..)
|
||||||
, mkExactFilter, mkExactFilterWith
|
, mkExactFilter, mkExactFilterWith
|
||||||
, mkContainsFilter
|
, mkContainsFilter, mkContainsFilterWith
|
||||||
, mkExistsFilter
|
, mkExistsFilter
|
||||||
, anyFilter, allFilter
|
, anyFilter, allFilter
|
||||||
) where
|
) where
|
||||||
@ -40,12 +40,18 @@ isJust :: (E.Esqueleto query expr backend, PersistField typ) => expr (E.Value (M
|
|||||||
isJust = E.not_ . E.isNothing
|
isJust = E.not_ . E.isNothing
|
||||||
|
|
||||||
-- | Check if the first string is contained in the text derived from the second argument
|
-- | Check if the first string is contained in the text derived from the second argument
|
||||||
isInfixOf :: (E.Esqueleto query expr backend, E.SqlString s2) =>
|
isInfixOf :: ( E.Esqueleto query expr backend
|
||||||
Text -> expr (E.Value s2) -> expr (E.Value Bool)
|
, E.SqlString s1
|
||||||
isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. E.val needle E.++. (E.%)
|
, E.SqlString s2
|
||||||
|
)
|
||||||
|
=> expr (E.Value s1) -> expr (E.Value s2) -> expr (E.Value Bool)
|
||||||
|
isInfixOf needle strExpr = E.castString strExpr `E.ilike` (E.%) E.++. needle E.++. (E.%)
|
||||||
|
|
||||||
hasInfix :: (E.Esqueleto query expr backend, E.SqlString s2) =>
|
hasInfix :: ( E.Esqueleto query expr backend
|
||||||
expr (E.Value s2) -> Text -> expr (E.Value Bool)
|
, E.SqlString s1
|
||||||
|
, E.SqlString s2
|
||||||
|
)
|
||||||
|
=> expr (E.Value s2) -> expr (E.Value s1) -> expr (E.Value Bool)
|
||||||
hasInfix = flip isInfixOf
|
hasInfix = flip isInfixOf
|
||||||
|
|
||||||
-- | Given a test and a set of values, check whether anyone succeeds the test
|
-- | Given a test and a set of values, check whether anyone succeeds the test
|
||||||
@ -101,14 +107,23 @@ mkExactFilterWith cast lenslike row criterias
|
|||||||
-- | generic filter creation for dbTable
|
-- | generic filter creation for dbTable
|
||||||
-- Given a lens-like function, make filter searching for needles in String-like elements
|
-- Given a lens-like function, make filter searching for needles in String-like elements
|
||||||
-- (Keep Set here to ensure that there are no duplicates)
|
-- (Keep Set here to ensure that there are no duplicates)
|
||||||
mkContainsFilter :: (E.SqlString a)
|
mkContainsFilter :: E.SqlString a
|
||||||
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
=> (t -> E.SqlExpr (E.Value a)) -- ^ getter from query to searched element
|
||||||
-> t -- ^ query row
|
-> t -- ^ query row
|
||||||
-> Set.Set Text -- ^ needle collection
|
-> Set.Set a -- ^ needle collection
|
||||||
-> E.SqlExpr (E.Value Bool)
|
-> E.SqlExpr (E.Value Bool)
|
||||||
mkContainsFilter lenslike row criterias
|
mkContainsFilter = mkContainsFilterWith id
|
||||||
|
|
||||||
|
-- | like `mkContainsFiler` but allows for conversion; convenient in conjunction with `anyFilter` and `allFilter`
|
||||||
|
mkContainsFilterWith :: E.SqlString b
|
||||||
|
=> (a -> b)
|
||||||
|
-> (t -> E.SqlExpr (E.Value b)) -- ^ getter from query to searched element
|
||||||
|
-> t -- ^ query row
|
||||||
|
-> Set.Set a -- ^ needle collection
|
||||||
|
-> E.SqlExpr (E.Value Bool)
|
||||||
|
mkContainsFilterWith cast lenslike row criterias
|
||||||
| Set.null criterias = true
|
| Set.null criterias = true
|
||||||
| otherwise = any (hasInfix $ lenslike row) criterias
|
| otherwise = any (hasInfix $ lenslike row) (E.val . cast <$> Set.toList criterias)
|
||||||
|
|
||||||
mkExistsFilter :: PathPiece a
|
mkExistsFilter :: PathPiece a
|
||||||
=> (t -> a -> E.SqlQuery ())
|
=> (t -> a -> E.SqlQuery ())
|
||||||
|
|||||||
@ -689,7 +689,7 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
|
|||||||
_ -> return ()
|
_ -> return ()
|
||||||
|
|
||||||
return Authorized
|
return Authorized
|
||||||
|
|
||||||
CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do
|
CTutorialR tid ssh csh tutn TRegisterR -> maybeT (unauthorizedI MsgUnauthorizedTutorialTime) $ do
|
||||||
now <- liftIO getCurrentTime
|
now <- liftIO getCurrentTime
|
||||||
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
|
||||||
@ -2374,6 +2374,14 @@ pageActions (CSheetR tid ssh csh shn SCorrR) =
|
|||||||
, menuItemModal = False
|
, menuItemModal = False
|
||||||
, menuItemAccessCallback' = return True
|
, menuItemAccessCallback' = return True
|
||||||
}
|
}
|
||||||
|
, MenuItem
|
||||||
|
{ menuItemType = PageActionPrime
|
||||||
|
, menuItemLabel = MsgMenuCorrectionsAssign
|
||||||
|
, menuItemIcon = Nothing
|
||||||
|
, menuItemRoute = SomeRoute $ CSheetR tid ssh csh shn SAssignR
|
||||||
|
, menuItemModal = False
|
||||||
|
, menuItemAccessCallback' = return True
|
||||||
|
}
|
||||||
, MenuItem
|
, MenuItem
|
||||||
{ menuItemType = PageActionSecondary
|
{ menuItemType = PageActionSecondary
|
||||||
, menuItemLabel = MsgMenuSheetEdit
|
, menuItemLabel = MsgMenuSheetEdit
|
||||||
|
|||||||
@ -388,6 +388,8 @@ postAdminFeaturesR = do
|
|||||||
}
|
}
|
||||||
psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
psValidator = def -- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
||||||
& defaultSorting [SortAscBy "key"]
|
& defaultSorting [SortAscBy "key"]
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in dbTable psValidator DBTable{..}
|
in dbTable psValidator DBTable{..}
|
||||||
|
|
||||||
mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget)
|
mkStudytermsTable :: Set (Key StudyTerms) -> Set (Key StudyTerms) -> DB (FormResult (DBFormResult (Key StudyTerms) (Maybe Text, Maybe Text) (DBRow (Entity StudyTerms))), Widget)
|
||||||
@ -421,6 +423,8 @@ postAdminFeaturesR = do
|
|||||||
psValidator = def
|
psValidator = def
|
||||||
-- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
-- & defaultSorting [SortAscBy "name", SortAscBy "short", SortAscBy "key"]
|
||||||
& defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"]
|
& defaultSorting [SortDescBy "isnew", SortDescBy "isbad", SortAscBy "key"]
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in dbTable psValidator DBTable{..}
|
in dbTable psValidator DBTable{..}
|
||||||
|
|
||||||
mkCandidateTable =
|
mkCandidateTable =
|
||||||
@ -454,5 +458,7 @@ postAdminFeaturesR = do
|
|||||||
]
|
]
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
psValidator = def & defaultSorting [SortAscBy "incidence", SortAscBy "key", SortAscBy "name"]
|
psValidator = def & defaultSorting [SortAscBy "incidence", SortAscBy "key", SortAscBy "name"]
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in dbTable psValidator DBTable{..}
|
in dbTable psValidator DBTable{..}
|
||||||
|
|
||||||
|
|||||||
@ -348,9 +348,9 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
)
|
)
|
||||||
, ( "corrector-name-email" -- corrector filter does not work for text-filtering
|
, ( "corrector-name-email" -- corrector filter does not work for text-filtering
|
||||||
, FilterColumn $ E.anyFilter
|
, FilterColumn $ E.anyFilter
|
||||||
[ E.mkContainsFilter $ queryCorrector >>> (E.?. UserSurname)
|
[ E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserSurname)
|
||||||
, E.mkContainsFilter $ queryCorrector >>> (E.?. UserDisplayName)
|
, E.mkContainsFilterWith Just $ queryCorrector >>> (E.?. UserDisplayName)
|
||||||
, E.mkContainsFilter $ queryCorrector >>> (E.?. UserEmail)
|
, E.mkContainsFilterWith (Just . CI.mk) $ queryCorrector >>> (E.?. UserEmail)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
, ( "user-name-email"
|
, ( "user-name-email"
|
||||||
@ -360,7 +360,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
E.where_ $ (\f -> f user $ Set.singleton needle) $ E.anyFilter
|
E.where_ $ (\f -> f user $ Set.singleton needle) $ E.anyFilter
|
||||||
[ E.mkContainsFilter (E.^. UserSurname)
|
[ E.mkContainsFilter (E.^. UserSurname)
|
||||||
, E.mkContainsFilter (E.^. UserDisplayName)
|
, E.mkContainsFilter (E.^. UserDisplayName)
|
||||||
, E.mkContainsFilter (E.^. UserEmail)
|
, E.mkContainsFilterWith CI.mk (E.^. UserEmail)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
, ( "user-matriclenumber"
|
, ( "user-matriclenumber"
|
||||||
@ -380,6 +380,8 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
|
|||||||
, dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI }
|
, dbtStyle = def { dbsFilterLayout = maybe (\_ _ _ -> id) (\_ -> defaultDBSFilterLayout) dbtFilterUI }
|
||||||
, dbtParams
|
, dbtParams
|
||||||
, dbtIdent = "corrections" :: Text
|
, dbtIdent = "corrections" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
data ActionCorrections = CorrDownload
|
data ActionCorrections = CorrDownload
|
||||||
@ -564,6 +566,8 @@ assignAction selId = ( CorrSetCorrector
|
|||||||
|
|
||||||
E.where_ $ either (\cId -> course E.^. CourseId E.==. E.val cId) (\shId -> sheet E.^. SheetId E.==. E.val shId) selId
|
E.where_ $ either (\cId -> course E.^. CourseId E.==. E.val cId) (\shId -> sheet E.^. SheetId E.==. E.val shId) selId
|
||||||
|
|
||||||
|
E.orderBy $ [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName]
|
||||||
|
|
||||||
E.distinct $ return user
|
E.distinct $ return user
|
||||||
|
|
||||||
correctors' <- forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (SomeMessage userDisplayName, ) <$> encrypt entityKey
|
correctors' <- forM correctors $ \Entity{ entityKey, entityVal = User{..} } -> (SomeMessage userDisplayName, ) <$> encrypt entityKey
|
||||||
@ -1136,17 +1140,24 @@ assignHandler tid ssh csh cid assignSids = do
|
|||||||
E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
||||||
E.where_ $ corrector E.^. SheetCorrectorSheet `E.in_` E.valList sheetIds
|
E.where_ $ corrector E.^. SheetCorrectorSheet `E.in_` E.valList sheetIds
|
||||||
return (corrector, user)
|
return (corrector, user)
|
||||||
let correctorMap :: Map UserId (User, Map SheetName SheetCorrector)
|
let correctorMap' :: Map UserId (User, Map SheetName SheetCorrector)
|
||||||
correctorMap = (\f -> foldl f Map.empty correctors) (\acc (Entity _ sheetcorr@SheetCorrector{sheetCorrectorSheet}, Entity uid user) ->
|
correctorMap' = (\f -> foldl f Map.empty correctors)
|
||||||
let shn = sheetName $ sheets ! sheetCorrectorSheet
|
(\acc (Entity _ sheetcorr@SheetCorrector{sheetCorrectorSheet}, Entity uid user) ->
|
||||||
in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc
|
let shn = sheetName $ sheets ! sheetCorrectorSheet
|
||||||
|
in Map.insertWith (\(usr, ma) (_, mb) -> (usr, Map.union ma mb)) uid (user, Map.singleton shn sheetcorr) acc
|
||||||
|
)
|
||||||
|
-- Lecturers may correct without being enlisted SheetCorrectors, so fetch all names
|
||||||
|
act_correctors <- E.select . E.distinct . E.from $ \(submission `E.InnerJoin` user) -> do
|
||||||
|
E.on $ submission E.^. SubmissionRatingBy E.==. (E.just $ user E.^. UserId)
|
||||||
|
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
|
||||||
|
return (submission E.^. SubmissionSheet, user)
|
||||||
|
let correctorMap :: Map UserId (User, Map SheetName SheetCorrector)
|
||||||
|
correctorMap = (\f -> foldl f correctorMap' act_correctors)
|
||||||
|
(\acc (E.Value sheetCorrectorSheet, Entity uid user) ->
|
||||||
|
let shn = sheetName $ sheets ! sheetCorrectorSheet
|
||||||
|
scr = SheetCorrector uid sheetCorrectorSheet mempty CorrectorExcused
|
||||||
|
in Map.insertWith (\_new old -> old) uid (user, Map.singleton shn scr) acc -- keep already known correctors unchanged
|
||||||
)
|
)
|
||||||
|
|
||||||
-- -- lecturerNames :: Map UserId User
|
|
||||||
-- lecturerNames <- fmap entities2map $ E.select $ E.from $ \(user `E.InnerJoin` lecturer) -> do
|
|
||||||
-- E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
|
|
||||||
-- E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
|
|
||||||
-- return user
|
|
||||||
|
|
||||||
submissions <- E.select . E.from $ \submission -> do
|
submissions <- E.select . E.from $ \submission -> do
|
||||||
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
|
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
|
||||||
|
|||||||
@ -205,6 +205,8 @@ makeCourseTable whereClause colChoices psValidator = do
|
|||||||
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "courses" :: Text
|
, dbtIdent = "courses" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
getCourseListR :: Handler Html
|
getCourseListR :: Handler Html
|
||||||
@ -402,6 +404,8 @@ getCShowR tid ssh csh = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "tutorials"
|
dbtIdent = "tutorials"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
tutorialDBTableValidator = def
|
tutorialDBTableValidator = def
|
||||||
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
||||||
@ -459,6 +463,8 @@ getCShowR tid ssh csh = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exams"
|
dbtIdent = "exams"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
examDBTableValidator = def
|
examDBTableValidator = def
|
||||||
& defaultSorting [SortAscBy "time"]
|
& defaultSorting [SortAscBy "time"]
|
||||||
@ -1140,13 +1146,13 @@ makeCourseUserTable cid restrict colChoices psValidator = do
|
|||||||
, ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
, ("field-short" , FilterColumn $ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand))
|
||||||
, ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey))
|
, ("field-key" , FilterColumn $ E.mkExactFilter $ queryFeaturesField >>> (E.?. StudyTermsKey))
|
||||||
, ("field" , FilterColumn $ E.anyFilter
|
, ("field" , FilterColumn $ E.anyFilter
|
||||||
[ E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsName)
|
[ E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsName)
|
||||||
, E.mkContainsFilter $ queryFeaturesField >>> (E.?. StudyTermsShorthand)
|
, E.mkContainsFilterWith Just $ queryFeaturesField >>> E.joinV . (E.?. StudyTermsShorthand)
|
||||||
, E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey)
|
, E.mkExactFilterWith readMay $ queryFeaturesField >>> (E.?. StudyTermsKey)
|
||||||
] )
|
] )
|
||||||
, ("degree" , FilterColumn $ E.anyFilter
|
, ("degree" , FilterColumn $ E.anyFilter
|
||||||
[ E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeName)
|
[ E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeName)
|
||||||
, E.mkContainsFilter $ queryFeaturesDegree >>> (E.?. StudyDegreeShorthand)
|
, E.mkContainsFilterWith Just $ queryFeaturesDegree >>> E.joinV . (E.?. StudyDegreeShorthand)
|
||||||
, E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey)
|
, E.mkExactFilterWith readMay $ queryFeaturesDegree >>> (E.?. StudyDegreeKey)
|
||||||
] )
|
] )
|
||||||
, ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
, ("semesternr" , FilterColumn $ E.mkExactFilter $ queryFeaturesStudy >>> (E.?. StudyFeaturesSemester))
|
||||||
@ -1154,7 +1160,7 @@ makeCourseUserTable cid restrict colChoices psValidator = do
|
|||||||
E.from $ \(tutorial `E.InnerJoin` tutorialParticipant) -> do
|
E.from $ \(tutorial `E.InnerJoin` tutorialParticipant) -> do
|
||||||
E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
|
E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
|
||||||
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
E.where_ $ tutorial E.^. TutorialCourse E.==. E.val cid
|
||||||
E.&&. E.hasInfix (tutorial E.^. TutorialName) criterion
|
E.&&. E.hasInfix (tutorial E.^. TutorialName) (E.val criterion :: E.SqlExpr (E.Value (CI Text)))
|
||||||
E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. queryUser row E.^. UserId
|
E.&&. tutorialParticipant E.^. TutorialParticipantUser E.==. queryUser row E.^. UserId
|
||||||
)
|
)
|
||||||
-- , ("course-registration", error "TODO") -- TODO
|
-- , ("course-registration", error "TODO") -- TODO
|
||||||
@ -1181,6 +1187,8 @@ makeCourseUserTable cid restrict colChoices psValidator = do
|
|||||||
, dbParamsFormResult = id
|
, dbParamsFormResult = id
|
||||||
, dbParamsFormIdent = def
|
, dbParamsFormIdent = def
|
||||||
}
|
}
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
over _1 postprocess <$> dbTable psValidator DBTable{..}
|
||||||
where
|
where
|
||||||
postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId)
|
postprocess :: FormResult (First act, DBFormResult UserId Bool UserTableData) -> FormResult (act, Set UserId)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import Handler.Utils.Exam
|
|||||||
import Handler.Utils.Invitations
|
import Handler.Utils.Invitations
|
||||||
import Handler.Utils.Table.Columns
|
import Handler.Utils.Table.Columns
|
||||||
import Handler.Utils.Table.Cells
|
import Handler.Utils.Table.Cells
|
||||||
|
import Handler.Utils.Csv
|
||||||
import Jobs.Queue
|
import Jobs.Queue
|
||||||
|
|
||||||
import Utils.Lens hiding (parts)
|
import Utils.Lens hiding (parts)
|
||||||
@ -29,6 +30,10 @@ import qualified Data.CaseInsensitive as CI
|
|||||||
|
|
||||||
import qualified Control.Monad.State.Class as State
|
import qualified Control.Monad.State.Class as State
|
||||||
|
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
|
||||||
getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
getCExamListR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
|
||||||
getCExamListR tid ssh csh = do
|
getCExamListR tid ssh csh = do
|
||||||
@ -74,6 +79,8 @@ getCExamListR tid ssh csh = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exams"
|
dbtIdent = "exams"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
examDBTableValidator = def
|
examDBTableValidator = def
|
||||||
& defaultSorting [SortAscBy "time"]
|
& defaultSorting [SortAscBy "time"]
|
||||||
@ -733,8 +740,8 @@ getEShowR tid ssh csh examn = do
|
|||||||
examBonusW bonusRule = $(widgetFile "widgets/bonusRule")
|
examBonusW bonusRule = $(widgetFile "widgets/bonusRule")
|
||||||
$(widgetFile "exam-show")
|
$(widgetFile "exam-show")
|
||||||
|
|
||||||
type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence))
|
type ExamUserTableExpr = (E.SqlExpr (Entity ExamRegistration) `E.InnerJoin` E.SqlExpr (Entity User)) `E.LeftOuterJoin` E.SqlExpr (Maybe (Entity ExamOccurrence)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity CourseParticipant)) `E.LeftOuterJoin` (E.SqlExpr (Maybe (Entity StudyFeatures)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyDegree)) `E.InnerJoin` E.SqlExpr (Maybe (Entity StudyTerms))))
|
||||||
type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence))
|
type ExamUserTableData = DBRow (Entity ExamRegistration, Entity User, Maybe (Entity ExamOccurrence), Maybe (Entity StudyFeatures), Maybe (Entity StudyDegree), Maybe (Entity StudyTerms))
|
||||||
|
|
||||||
instance HasEntity ExamUserTableData User where
|
instance HasEntity ExamUserTableData User where
|
||||||
hasEntity = _dbrOutput . _2
|
hasEntity = _dbrOutput . _2
|
||||||
@ -746,47 +753,124 @@ _userTableOccurrence :: Lens' ExamUserTableData (Maybe (Entity ExamOccurrence))
|
|||||||
_userTableOccurrence = _dbrOutput . _3
|
_userTableOccurrence = _dbrOutput . _3
|
||||||
|
|
||||||
queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User)
|
queryUser :: ExamUserTableExpr -> E.SqlExpr (Entity User)
|
||||||
queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 2 1)
|
queryUser = $(sqlIJproj 2 2) . $(sqlLOJproj 3 1)
|
||||||
|
|
||||||
|
queryStudyFeatures :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyFeatures))
|
||||||
|
queryStudyFeatures = $(sqlIJproj 3 1) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
|
||||||
|
|
||||||
queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration)
|
queryExamRegistration :: ExamUserTableExpr -> E.SqlExpr (Entity ExamRegistration)
|
||||||
queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 2 1)
|
queryExamRegistration = $(sqlIJproj 2 1) . $(sqlLOJproj 3 1)
|
||||||
|
|
||||||
|
queryStudyDegree :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyDegree))
|
||||||
|
queryStudyDegree = $(sqlIJproj 3 2) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
|
||||||
|
|
||||||
|
queryStudyField :: ExamUserTableExpr -> E.SqlExpr (Maybe (Entity StudyTerms))
|
||||||
|
queryStudyField = $(sqlIJproj 3 3) . $(sqlLOJproj 2 2) . $(sqlLOJproj 3 3)
|
||||||
|
|
||||||
|
resultUser :: Lens' ExamUserTableData (Entity User)
|
||||||
|
resultUser = _dbrOutput . _2
|
||||||
|
|
||||||
|
resultStudyFeatures :: Traversal' ExamUserTableData (Entity StudyFeatures)
|
||||||
|
resultStudyFeatures = _dbrOutput . _4 . _Just
|
||||||
|
|
||||||
|
resultStudyDegree :: Traversal' ExamUserTableData (Entity StudyDegree)
|
||||||
|
resultStudyDegree = _dbrOutput . _5 . _Just
|
||||||
|
|
||||||
|
resultStudyField :: Traversal' ExamUserTableData (Entity StudyTerms)
|
||||||
|
resultStudyField = _dbrOutput . _6 . _Just
|
||||||
|
|
||||||
|
resultExamOccurrence :: Traversal' ExamUserTableData (Entity ExamOccurrence)
|
||||||
|
resultExamOccurrence = _dbrOutput . _3 . _Just
|
||||||
|
|
||||||
|
data ExamUserTableCsv = ExamUserTableCsv
|
||||||
|
{ csvUserSurname :: Text
|
||||||
|
, csvUserName :: Text
|
||||||
|
, csvUserMatriculation :: Maybe Text
|
||||||
|
, csvUserField :: Maybe Text
|
||||||
|
, csvUserDegree :: Maybe Text
|
||||||
|
, csvUserSemester :: Maybe Int
|
||||||
|
, csvUserRoom :: Maybe Text
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
examUserTableCsvOptions :: Csv.Options
|
||||||
|
examUserTableCsvOptions = Csv.defaultOptions { Csv.fieldLabelModifier = camelToPathPiece' 1 }
|
||||||
|
|
||||||
|
instance ToNamedRecord ExamUserTableCsv where
|
||||||
|
toNamedRecord = Csv.genericToNamedRecord examUserTableCsvOptions
|
||||||
|
|
||||||
|
instance FromNamedRecord ExamUserTableCsv where
|
||||||
|
parseNamedRecord = Csv.genericParseNamedRecord examUserTableCsvOptions
|
||||||
|
|
||||||
|
instance DefaultOrdered ExamUserTableCsv where
|
||||||
|
headerOrder = Csv.genericHeaderOrder examUserTableCsvOptions
|
||||||
|
|
||||||
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
getEUsersR, postEUsersR :: TermId -> SchoolId -> CourseShorthand -> ExamName -> Handler Html
|
||||||
getEUsersR = postEUsersR
|
getEUsersR = postEUsersR
|
||||||
postEUsersR tid ssh csh examn = do
|
postEUsersR tid ssh csh examn = do
|
||||||
eid <- runDB $ fetchExamId tid ssh csh examn
|
Entity eid Exam{..} <- runDB $ fetchExam tid ssh csh examn
|
||||||
|
|
||||||
let
|
let
|
||||||
examUsersDBTable = DBTable{..}
|
examUsersDBTable = DBTable{..}
|
||||||
where
|
where
|
||||||
dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence) = do
|
dbtSQLQuery ((examRegistration `E.InnerJoin` user) `E.LeftOuterJoin` occurrence `E.LeftOuterJoin` (courseParticipant `E.LeftOuterJoin` (studyFeatures `E.InnerJoin` studyDegree `E.InnerJoin` studyField))) = do
|
||||||
|
E.on $ studyField E.?. StudyTermsId E.==. studyFeatures E.?. StudyFeaturesField
|
||||||
|
E.on $ studyDegree E.?. StudyDegreeId E.==. studyFeatures E.?. StudyFeaturesDegree
|
||||||
|
E.on $ studyFeatures E.?. StudyFeaturesId E.==. E.joinV (courseParticipant E.?. CourseParticipantField)
|
||||||
|
E.on $ courseParticipant E.?. CourseParticipantCourse E.==. E.just (E.val examCourse)
|
||||||
|
E.&&. courseParticipant E.?. CourseParticipantUser E.==. E.just (user E.^. UserId)
|
||||||
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
|
E.on $ occurrence E.?. ExamOccurrenceExam E.==. E.just (E.val eid)
|
||||||
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
|
E.&&. occurrence E.?. ExamOccurrenceId E.==. examRegistration E.^. ExamRegistrationOccurrence
|
||||||
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
|
E.on $ examRegistration E.^. ExamRegistrationUser E.==. user E.^. UserId
|
||||||
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid
|
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. E.val eid
|
||||||
return (examRegistration, user, occurrence)
|
return (examRegistration, user, occurrence, studyFeatures, studyDegree, studyField)
|
||||||
dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId)
|
dbtRowKey = queryExamRegistration >>> (E.^. ExamRegistrationId)
|
||||||
dbtProj = return
|
dbtProj = return
|
||||||
dbtColonnade = dbColonnade $ mconcat
|
dbtColonnade = dbColonnade $ mconcat
|
||||||
[ colUserNameLink (CourseR tid ssh csh . CUserR)
|
[ colUserNameLink (CourseR tid ssh csh . CUserR)
|
||||||
, colUserMatriclenr
|
, colUserMatriclenr
|
||||||
-- , colUserDegreeShort
|
, colField resultStudyField
|
||||||
-- , colUserField
|
, colDegreeShort resultStudyDegree
|
||||||
-- , colUserSemester
|
, colFeaturesSemester resultStudyFeatures
|
||||||
, sortable (Just "room") (i18nCell MsgExamRoom) (maybe mempty (cell . toWgt . examOccurrenceRoom . entityVal) . view _userTableOccurrence)
|
, sortable (Just "room") (i18nCell MsgExamRoom) (maybe mempty (cell . toWgt . examOccurrenceRoom . entityVal) . view _userTableOccurrence)
|
||||||
]
|
]
|
||||||
dbtSorting = Map.fromList
|
dbtSorting = Map.fromList
|
||||||
[ sortUserNameLink queryUser
|
[ sortUserNameLink queryUser
|
||||||
, sortUserSurname queryUser
|
, sortUserSurname queryUser
|
||||||
, sortUserDisplayName queryUser
|
, sortUserDisplayName queryUser
|
||||||
, sortUserMatriclenr queryUser
|
, sortUserMatriclenr queryUser
|
||||||
|
, sortField queryStudyField
|
||||||
|
, sortDegreeShort queryStudyDegree
|
||||||
|
, sortFeaturesSemester queryStudyFeatures
|
||||||
]
|
]
|
||||||
dbtFilter = Map.empty
|
dbtFilter = Map.fromList
|
||||||
dbtFilterUI = const mempty
|
[ fltrUserNameEmail queryUser
|
||||||
dbtStyle = def
|
, fltrUserMatriclenr queryUser
|
||||||
|
, fltrField queryStudyField
|
||||||
|
, fltrDegree queryStudyDegree
|
||||||
|
, fltrFeaturesSemester queryStudyFeatures
|
||||||
|
]
|
||||||
|
dbtFilterUI mPrev = mconcat
|
||||||
|
[ fltrUserNameEmailUI mPrev
|
||||||
|
, fltrUserMatriclenrUI mPrev
|
||||||
|
, fltrFieldUI mPrev
|
||||||
|
, fltrDegreeUI mPrev
|
||||||
|
, fltrFeaturesSemesterUI mPrev
|
||||||
|
]
|
||||||
|
dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exam-users"
|
dbtIdent = "exam-users"
|
||||||
|
dbtCsvEncode :: DBTCsvEncode ExamUserTableData ExamUserTableCsv
|
||||||
|
dbtCsvEncode = DictJust . C.map $ ExamUserTableCsv
|
||||||
|
<$> view (resultUser . _entityVal . _userSurname)
|
||||||
|
<*> view (resultUser . _entityVal . _userDisplayName)
|
||||||
|
<*> view (resultUser . _entityVal . _userMatrikelnummer)
|
||||||
|
<*> preview (resultStudyField . _entityVal . to (\StudyTerms{..} -> studyTermsName <|> studyTermsShorthand <|> Just (tshow studyTermsKey)) . _Just)
|
||||||
|
<*> preview (resultStudyDegree . _entityVal . to (\StudyDegree{..} -> studyDegreeName <|> studyDegreeShorthand <|> Just (tshow studyDegreeKey)) . _Just)
|
||||||
|
<*> preview (resultStudyFeatures . _entityVal . _studyFeaturesSemester)
|
||||||
|
<*> preview (resultExamOccurrence . _entityVal . _examOccurrenceRoom)
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
examUsersDBTableValidator = def
|
examUsersDBTableValidator = def
|
||||||
((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable
|
((), examUsersTable) <- runDB $ dbTable examUsersDBTableValidator examUsersDBTable
|
||||||
|
|||||||
@ -80,6 +80,8 @@ homeOpenCourses = do
|
|||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "open-courses" :: Text
|
, dbtIdent = "open-courses" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
$(widgetFile "home/openCourses")
|
$(widgetFile "home/openCourses")
|
||||||
|
|
||||||
@ -179,6 +181,8 @@ homeUpcomingSheets uid = do
|
|||||||
, dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines }
|
, dbtStyle = def { dbsEmptyStyle = DBESNoHeading, dbsEmptyMessage = MsgNoUpcomingSheetDeadlines }
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "upcoming-sheets" :: Text
|
, dbtIdent = "upcoming-sheets" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
$(widgetFile "home/upcomingSheets")
|
$(widgetFile "home/upcomingSheets")
|
||||||
|
|
||||||
@ -286,6 +290,8 @@ homeUpcomingExams uid = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "exams"
|
dbtIdent = "exams"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
examDBTableValidator = def
|
examDBTableValidator = def
|
||||||
& defaultSorting [SortAscBy "time"]
|
& defaultSorting [SortAscBy "time"]
|
||||||
|
|||||||
@ -149,6 +149,8 @@ getMaterialListR tid ssh csh = do
|
|||||||
]
|
]
|
||||||
, dbtFilter = mempty
|
, dbtFilter = mempty
|
||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
let headingLong = prependCourseTitle tid ssh csh MsgMaterialListHeading
|
let headingLong = prependCourseTitle tid ssh csh MsgMaterialListHeading
|
||||||
@ -219,6 +221,8 @@ getMShowR tid ssh csh mnm = do
|
|||||||
[ sortFilePath $(sqlIJproj 2 2)
|
[ sortFilePath $(sqlIJproj 2 2)
|
||||||
, sortFileModification $(sqlIJproj 2 2)
|
, sortFileModification $(sqlIJproj 2 2)
|
||||||
]
|
]
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
return (matEnt,fileTable')
|
return (matEnt,fileTable')
|
||||||
|
|
||||||
|
|||||||
@ -258,6 +258,8 @@ mkOwnedCoursesTable =
|
|||||||
]
|
]
|
||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid in (_1 %~ getAny) <$> dbTableWidget validator DBTable{..}
|
||||||
|
|
||||||
|
|
||||||
@ -308,6 +310,8 @@ mkEnrolledCoursesTable =
|
|||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -387,6 +391,8 @@ mkSubmissionTable =
|
|||||||
]
|
]
|
||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
dbtSorting = dbtSorting' uid
|
dbtSorting = dbtSorting' uid
|
||||||
in dbTableWidget' validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
@ -459,6 +465,8 @@ mkSubmissionGroupTable =
|
|||||||
]
|
]
|
||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
in dbTableWidget' validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
|
||||||
@ -535,6 +543,8 @@ mkCorrectionsTable =
|
|||||||
]
|
]
|
||||||
dbtFilterUI = mempty
|
dbtFilterUI = mempty
|
||||||
dbtParams = def
|
dbtParams = def
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
in \uid -> let dbtSQLQuery = dbtSQLQuery' uid
|
||||||
in dbTableWidget' validator DBTable{..}
|
in dbTableWidget' validator DBTable{..}
|
||||||
|
|
||||||
|
|||||||
@ -310,6 +310,8 @@ getSheetListR tid ssh csh = do
|
|||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "sheets" :: Text
|
, dbtIdent = "sheets" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
-- ) ( -- !!!DEPRECTAED!!! Summary only over shown rows !!!
|
-- ) ( -- !!!DEPRECTAED!!! Summary only over shown rows !!!
|
||||||
-- -- Collect summary over all Sheets, not just the ones shown due to pagination:
|
-- -- Collect summary over all Sheets, not just the ones shown due to pagination:
|
||||||
@ -404,6 +406,8 @@ getSShowR tid ssh csh shn = do
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
(hasHints, hasSolution) <- runDB $ do
|
(hasHints, hasSolution) <- runDB $ do
|
||||||
hasHints <- (/= 0) <$> count [ SheetFileSheet ==. sid, SheetFileType ==. SheetHint ]
|
hasHints <- (/= 0) <$> count [ SheetFileSheet ==. sid, SheetFileType ==. SheetHint ]
|
||||||
@ -731,6 +735,7 @@ correctorForm shid = wFormToAForm $ do
|
|||||||
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
|
E.on $ sheet E.^. SheetId E.==. sheetCorrector E.^. SheetCorrectorSheet
|
||||||
E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId
|
||||||
E.where_ $ lecturer E.^. LecturerUser E.==. E.val userId
|
E.where_ $ lecturer E.^. LecturerUser E.==. E.val userId
|
||||||
|
E.orderBy $ [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName]
|
||||||
return user
|
return user
|
||||||
|
|
||||||
miAdd :: ListPosition
|
miAdd :: ListPosition
|
||||||
|
|||||||
@ -520,6 +520,8 @@ submissionHelper tid ssh csh shn mcid = do
|
|||||||
, dbtFilter = mempty
|
, dbtFilter = mempty
|
||||||
, dbtFilterUI = mempty
|
, dbtFilterUI = mempty
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
|
mFileTable <- traverse (runDB . dbTableWidget' def) . fmap smid2ArchiveTable $ (,) <$> msmid <*> mcid
|
||||||
|
|
||||||
|
|||||||
@ -224,6 +224,8 @@ postMessageListR = do
|
|||||||
, dbParamsFormIdent = def
|
, dbParamsFormIdent = def
|
||||||
}
|
}
|
||||||
, dbtIdent = "messages" :: Text
|
, dbtIdent = "messages" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)
|
let tableRes = tableRes' & mapped._2 %~ Map.keysSet . Map.filter id . getDBFormResult (const False)
|
||||||
|
|||||||
@ -145,6 +145,8 @@ getTermShowR = do
|
|||||||
, dbtStyle = def
|
, dbtStyle = def
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "terms" :: Text
|
, dbtIdent = "terms" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
setTitleI MsgTermsHeading
|
setTitleI MsgTermsHeading
|
||||||
|
|||||||
@ -93,6 +93,8 @@ getCTutorialListR tid ssh csh = do
|
|||||||
dbtParams = def
|
dbtParams = def
|
||||||
dbtIdent :: Text
|
dbtIdent :: Text
|
||||||
dbtIdent = "tutorials"
|
dbtIdent = "tutorials"
|
||||||
|
dbtCsvEncode = noCsvEncode
|
||||||
|
dbtCsvDecode = Nothing
|
||||||
|
|
||||||
tutorialDBTableValidator = def
|
tutorialDBTableValidator = def
|
||||||
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
& defaultSorting [SortAscBy "type", SortAscBy "name"]
|
||||||
|
|||||||
@ -108,14 +108,14 @@ getUsersR = do
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates
|
, dbtFilter = Map.fromList -- OverloadedLists does not work with the templates
|
||||||
[ ( "user-search", FilterColumn $ \user criterion ->
|
[ ( "user-search", FilterColumn $ \user (criteria :: Set.Set Text) ->
|
||||||
if Set.null criterion then E.true else -- TODO: why is this condition not needed?
|
if Set.null criteria then E.true else -- TODO: why is this condition not needed?
|
||||||
-- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text)
|
-- Set.foldr (\needle acc -> acc E.||. (user E.^. UserDisplayName) `E.hasInfix` needle) eFalse (criterion :: Set.Set Text)
|
||||||
E.any (user E.^. UserDisplayName `E.hasInfix`) criterion
|
E.any (\c -> user E.^. UserDisplayName `E.hasInfix` E.val c) criteria
|
||||||
)
|
)
|
||||||
, ( "matriculation", FilterColumn $ \user (criterion :: Set.Set Text) -> if
|
, ( "matriculation", FilterColumn $ \user (criteria :: Set.Set Text) -> if
|
||||||
| Set.null criterion -> E.true -- TODO: why can this be eFalse and work still?
|
| Set.null criteria -> E.true -- TODO: why can this be eFalse and work still?
|
||||||
| otherwise -> E.any (user E.^. UserMatrikelnummer `E.hasInfix`) criterion
|
| otherwise -> E.any (\c -> user E.^. UserMatrikelnummer `E.hasInfix` E.val c) criteria
|
||||||
)
|
)
|
||||||
, ( "school", FilterColumn $ \user criterion -> if
|
, ( "school", FilterColumn $ \user criterion -> if
|
||||||
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
|
| Set.null criterion -> E.val True :: E.SqlExpr (E.Value Bool)
|
||||||
@ -140,6 +140,8 @@ getUsersR = do
|
|||||||
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
|
||||||
, dbtParams = def
|
, dbtParams = def
|
||||||
, dbtIdent = "users" :: Text
|
, dbtIdent = "users" :: Text
|
||||||
|
, dbtCsvEncode = noCsvEncode
|
||||||
|
, dbtCsvDecode = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultLayout $ do
|
defaultLayout $ do
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import Handler.Utils.Rating as Handler.Utils hiding (extractRatings)
|
|||||||
-- import Handler.Utils.Submission as Handler.Utils
|
-- import Handler.Utils.Submission as Handler.Utils
|
||||||
import Handler.Utils.Sheet as Handler.Utils
|
import Handler.Utils.Sheet as Handler.Utils
|
||||||
import Handler.Utils.Mail as Handler.Utils
|
import Handler.Utils.Mail as Handler.Utils
|
||||||
|
import Handler.Utils.ContentDisposition as Handler.Utils
|
||||||
|
|
||||||
import System.Directory (listDirectory)
|
import System.Directory (listDirectory)
|
||||||
import System.FilePath.Posix (takeBaseName, takeFileName)
|
import System.FilePath.Posix (takeBaseName, takeFileName)
|
||||||
@ -41,21 +42,6 @@ import qualified Data.List.NonEmpty as NonEmpty
|
|||||||
import Control.Monad.Logger
|
import Control.Monad.Logger
|
||||||
|
|
||||||
|
|
||||||
-- | Check whether the user's preference for files is inline-viewing or downloading
|
|
||||||
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
|
|
||||||
downloadFiles = do
|
|
||||||
mauth <- liftHandlerT maybeAuth
|
|
||||||
case mauth of
|
|
||||||
Just (Entity _ User{..}) -> return userDownloadFiles
|
|
||||||
Nothing -> do
|
|
||||||
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
|
|
||||||
return userDefaultDownloadFiles
|
|
||||||
|
|
||||||
setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m ()
|
|
||||||
setContentDisposition' mFileName = do
|
|
||||||
wantsDownload <- downloadFiles
|
|
||||||
setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName
|
|
||||||
|
|
||||||
-- | Simply send a `File`-Value
|
-- | Simply send a `File`-Value
|
||||||
sendThisFile :: File -> Handler TypedContent
|
sendThisFile :: File -> Handler TypedContent
|
||||||
sendThisFile File{..}
|
sendThisFile File{..}
|
||||||
|
|||||||
24
src/Handler/Utils/ContentDisposition.hs
Normal file
24
src/Handler/Utils/ContentDisposition.hs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
module Handler.Utils.ContentDisposition
|
||||||
|
( downloadFiles
|
||||||
|
, setContentDisposition'
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Utils.Lens
|
||||||
|
|
||||||
|
-- | Check whether the user's preference for files is inline-viewing or downloading
|
||||||
|
downloadFiles :: (MonadHandler m, HandlerSite m ~ UniWorX) => m Bool
|
||||||
|
downloadFiles = do
|
||||||
|
mauth <- liftHandlerT maybeAuth
|
||||||
|
case mauth of
|
||||||
|
Just (Entity _ User{..}) -> return userDownloadFiles
|
||||||
|
Nothing -> do
|
||||||
|
UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
|
||||||
|
return userDefaultDownloadFiles
|
||||||
|
|
||||||
|
setContentDisposition' :: (MonadHandler m, HandlerSite m ~ UniWorX) => Maybe FilePath -> m ()
|
||||||
|
setContentDisposition' mFileName = do
|
||||||
|
wantsDownload <- downloadFiles
|
||||||
|
setContentDisposition (bool ContentInline ContentAttachment wantsDownload) mFileName
|
||||||
|
|
||||||
71
src/Handler/Utils/Csv.hs
Normal file
71
src/Handler/Utils/Csv.hs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
|
module Handler.Utils.Csv
|
||||||
|
( typeCsv, extensionCsv
|
||||||
|
, decodeCsv
|
||||||
|
, encodeCsv
|
||||||
|
, respondCsv, respondCsvDB
|
||||||
|
, fileSourceCsv
|
||||||
|
, CsvParseError(..)
|
||||||
|
, ToNamedRecord(..), FromNamedRecord(..)
|
||||||
|
, DefaultOrdered(..)
|
||||||
|
, ToField(..), FromField(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
|
||||||
|
import Data.Csv
|
||||||
|
import Data.Csv.Conduit
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
import qualified Data.Conduit.Combinators as C (sourceLazy)
|
||||||
|
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
|
||||||
|
deriving instance Typeable CsvParseError
|
||||||
|
instance Exception CsvParseError
|
||||||
|
|
||||||
|
|
||||||
|
typeCsv :: ContentType
|
||||||
|
typeCsv = "text/csv"
|
||||||
|
|
||||||
|
extensionCsv :: Extension
|
||||||
|
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
|
||||||
|
|
||||||
|
|
||||||
|
decodeCsv :: (MonadThrow m, FromNamedRecord csv) => Conduit ByteString m csv
|
||||||
|
decodeCsv = transPipe throwExceptT $ fromNamedCsv defaultDecodeOptions
|
||||||
|
|
||||||
|
encodeCsv :: ( ToNamedRecord csv
|
||||||
|
, DefaultOrdered csv
|
||||||
|
, Monad m
|
||||||
|
)
|
||||||
|
=> Conduit csv m ByteString
|
||||||
|
-- ^ Encode a stream of records
|
||||||
|
--
|
||||||
|
-- Currently not streaming
|
||||||
|
encodeCsv = fmap encodeDefaultOrderedByName (C.foldMap pure) >>= C.sourceLazy
|
||||||
|
|
||||||
|
|
||||||
|
respondCsv :: ( ToNamedRecord csv
|
||||||
|
, DefaultOrdered csv
|
||||||
|
)
|
||||||
|
=> Source (HandlerT site IO) csv
|
||||||
|
-> HandlerT site IO TypedContent
|
||||||
|
respondCsv src = respondSource typeCsv $ src .| encodeCsv .| awaitForever sendChunk
|
||||||
|
|
||||||
|
respondCsvDB :: ( ToNamedRecord csv
|
||||||
|
, DefaultOrdered csv
|
||||||
|
, YesodPersistRunner site
|
||||||
|
)
|
||||||
|
=> Source (YesodDB site) csv
|
||||||
|
-> HandlerT site IO TypedContent
|
||||||
|
respondCsvDB src = respondSourceDB typeCsv $ src .| encodeCsv .| awaitForever sendChunk
|
||||||
|
|
||||||
|
fileSourceCsv :: ( FromNamedRecord csv
|
||||||
|
, MonadResource m
|
||||||
|
)
|
||||||
|
=> FileInfo
|
||||||
|
-> Source m csv
|
||||||
|
fileSourceCsv = (.| decodeCsv) . fileSource
|
||||||
@ -194,6 +194,18 @@ cellHasEMail :: (IsDBTable m a, HasUser u) => u -> DBCell m a
|
|||||||
cellHasEMail = emailCell . view _userEmail
|
cellHasEMail = emailCell . view _userEmail
|
||||||
|
|
||||||
|
|
||||||
|
cellHasSemester :: (IsDBTable m c, HasStudyFeatures a) => a -> DBCell m c
|
||||||
|
cellHasSemester = numCell . view _studyFeaturesSemester
|
||||||
|
|
||||||
|
|
||||||
|
cellHasField :: (IsDBTable m c, HasStudyTerms a) => a -> DBCell m c
|
||||||
|
cellHasField x = maybe (numCell $ x ^. _studyTermsKey) textCell $ x ^. _studyTermsName <|> x ^. _studyTermsShorthand
|
||||||
|
|
||||||
|
|
||||||
|
cellHasDegreeShort :: (IsDBTable m c, HasStudyDegree a) => a -> DBCell m c
|
||||||
|
cellHasDegreeShort x = maybe (numCell $ x ^. _studyDegreeKey) textCell $ x ^. _studyDegreeShorthand <|> x ^. _studyDegreeName
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Just for documentation purposes; inline this code instead:
|
-- Just for documentation purposes; inline this code instead:
|
||||||
maybeDateTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
|
maybeDateTimeCell :: IsDBTable m a => Maybe UTCTime -> DBCell m a
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import Import
|
|||||||
|
|
||||||
-- import Text.Blaze (ToMarkup(..))
|
-- import Text.Blaze (ToMarkup(..))
|
||||||
|
|
||||||
import Data.Monoid (Any(..))
|
|
||||||
import qualified Database.Esqueleto as E
|
import qualified Database.Esqueleto as E
|
||||||
import Database.Esqueleto.Utils as E
|
import Database.Esqueleto.Utils as E
|
||||||
|
|
||||||
@ -19,6 +18,8 @@ import Utils.Lens
|
|||||||
import Handler.Utils
|
import Handler.Utils
|
||||||
import Handler.Utils.Table.Cells
|
import Handler.Utils.Table.Cells
|
||||||
|
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
|
||||||
|
|
||||||
--------------------------------
|
--------------------------------
|
||||||
-- Generic Columns
|
-- Generic Columns
|
||||||
@ -121,7 +122,7 @@ sortUserDisplayName queryUser = ("user-display-name", SortColumn $ queryUser >>>
|
|||||||
defaultSortingByName :: PSValidator m x -> PSValidator m x
|
defaultSortingByName :: PSValidator m x -> PSValidator m x
|
||||||
defaultSortingByName =
|
defaultSortingByName =
|
||||||
-- defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters
|
-- defaultSorting [SortAscBy "user-surname", SortAscBy "user-display-name"] -- old way, requiring two exta sorters
|
||||||
defaultSorting [SortAscBy "user-name"] -- new way, untested, working with single sorter
|
defaultSorting [SortAscBy "user-name"] -- new way, working with single sorter
|
||||||
|
|
||||||
-- | Alias for sortUserName for consistency
|
-- | Alias for sortUserName for consistency
|
||||||
fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t)
|
fltrUserNameLink :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d) => (a -> E.SqlExpr (Entity User)) -> (d, FilterColumn t)
|
||||||
@ -156,9 +157,9 @@ fltrUserNameEmail :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool
|
|||||||
=> (a -> E.SqlExpr (Entity User))
|
=> (a -> E.SqlExpr (Entity User))
|
||||||
-> (d, FilterColumn t)
|
-> (d, FilterColumn t)
|
||||||
fltrUserNameEmail queryUser = ( "user-name-email", FilterColumn $ anyFilter
|
fltrUserNameEmail queryUser = ( "user-name-email", FilterColumn $ anyFilter
|
||||||
[ mkContainsFilter $ queryUser >>> (E.^. UserDisplayName)
|
[ mkContainsFilter $ queryUser >>> (E.^. UserDisplayName)
|
||||||
, mkContainsFilter $ queryUser >>> (E.^. UserSurname)
|
, mkContainsFilter $ queryUser >>> (E.^. UserSurname)
|
||||||
, mkContainsFilter $ queryUser >>> (E.^. UserEmail)
|
, mkContainsFilterWith CI.mk $ queryUser >>> (E.^. UserEmail)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -179,12 +180,14 @@ colUserMatriclenr :: (IsDBTable m c, HasUser a) => Colonnade Sortable a (DBCell
|
|||||||
colUserMatriclenr = sortable (Just "user-matriclenumber") (i18nCell MsgMatrikelNr) cellHasMatrikelnummer
|
colUserMatriclenr = sortable (Just "user-matriclenumber") (i18nCell MsgMatrikelNr) cellHasMatrikelnummer
|
||||||
|
|
||||||
sortUserMatriclenr :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t)
|
sortUserMatriclenr :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t)
|
||||||
sortUserMatriclenr queryUser = ( "user-matriclenumber", SortColumn $ queryUser >>> (E.^. UserMatrikelnummer))
|
sortUserMatriclenr queryUser = ("user-matriclenumber", SortColumn $ queryUser >>> (E.^. UserMatrikelnummer))
|
||||||
|
|
||||||
fltrUserMatriclenr :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d)
|
fltrUserMatriclenr :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool))
|
||||||
=> (a -> E.SqlExpr (Entity User))
|
, IsString d
|
||||||
-> (d, FilterColumn t)
|
)
|
||||||
fltrUserMatriclenr queryUser = ( "user-matriclenumber", FilterColumn $ mkContainsFilter $ queryUser >>> (E.^. UserMatrikelnummer))
|
=> (a -> E.SqlExpr (Entity User))
|
||||||
|
-> (d, FilterColumn t)
|
||||||
|
fltrUserMatriclenr queryUser = ("user-matriclenumber", FilterColumn . mkContainsFilterWith Just $ queryUser >>> (E.^. UserMatrikelnummer))
|
||||||
|
|
||||||
fltrUserMatriclenrUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
fltrUserMatriclenrUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
fltrUserMatriclenrUI mPrev =
|
fltrUserMatriclenrUI mPrev =
|
||||||
@ -199,13 +202,83 @@ colUserEmail = sortable (Just "user-email") (i18nCell MsgEMail) cellHasEMail
|
|||||||
sortUserEmail :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t)
|
sortUserEmail :: IsString d => (t -> E.SqlExpr (Entity User)) -> (d, SortColumn t)
|
||||||
sortUserEmail queryUser = ( "user-email", SortColumn $ queryUser >>> (E.^. UserEmail))
|
sortUserEmail queryUser = ( "user-email", SortColumn $ queryUser >>> (E.^. UserEmail))
|
||||||
|
|
||||||
fltrUserEmail :: (IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool)), IsString d)
|
fltrUserEmail :: ( IsFilterColumn t (a -> Set (CI Text) -> E.SqlExpr (E.Value Bool))
|
||||||
=> (a -> E.SqlExpr (Entity User))
|
, IsString d
|
||||||
-> (d, FilterColumn t)
|
)
|
||||||
fltrUserEmail queryUser = ( "user-email", FilterColumn $ mkContainsFilter $ queryUser >>> (E.^. UserEmail))
|
=> (a -> E.SqlExpr (Entity User))
|
||||||
|
-> (d, FilterColumn t)
|
||||||
|
fltrUserEmail queryUser = ("user-email", FilterColumn . mkContainsFilter $ queryUser >>> (E.^. UserEmail))
|
||||||
|
|
||||||
fltrUserEmailUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
fltrUserEmailUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
fltrUserEmailUI mPrev =
|
fltrUserEmailUI mPrev =
|
||||||
prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgEMail)
|
prismAForm (singletonFilter "user-email") mPrev $ aopt textField (fslI MsgEMail)
|
||||||
|
|
||||||
|
|
||||||
|
--------------------
|
||||||
|
-- Study features --
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
colFeaturesSemester :: (IsDBTable m c, HasStudyFeatures x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c)
|
||||||
|
colFeaturesSemester feature = sortable (Just "features-semester") (i18nCell MsgStudyFeatureAge) $ maybe mempty cellHasSemester . firstOf feature
|
||||||
|
|
||||||
|
sortFeaturesSemester :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyFeatures))) -> (d, SortColumn t)
|
||||||
|
sortFeaturesSemester queryFeatures = ("features-semester", SortColumn $ queryFeatures >>> (E.?. StudyFeaturesSemester))
|
||||||
|
|
||||||
|
fltrFeaturesSemester :: ( IsFilterColumn t (a -> Set Int -> E.SqlExpr (E.Value Bool))
|
||||||
|
, IsString d
|
||||||
|
)
|
||||||
|
=> (a -> E.SqlExpr (Maybe (Entity StudyFeatures)))
|
||||||
|
-> (d, FilterColumn t)
|
||||||
|
fltrFeaturesSemester queryFeatures = ("features-semester", FilterColumn . mkExactFilterWith Just $ queryFeatures >>> (E.?. StudyFeaturesSemester))
|
||||||
|
|
||||||
|
fltrFeaturesSemesterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
|
fltrFeaturesSemesterUI mPrev =
|
||||||
|
prismAForm (singletonFilter "features-semester" . maybePrism _PathPiece) mPrev $ aopt (intField :: Field (YesodDB UniWorX) Int) (fslI MsgStudyFeatureAge)
|
||||||
|
|
||||||
|
|
||||||
|
colField :: (IsDBTable m c, HasStudyTerms x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c)
|
||||||
|
colField terms = sortable (Just "terms") (i18nCell MsgStudyTerm) $ maybe mempty cellHasField . firstOf terms
|
||||||
|
|
||||||
|
sortField :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyTerms))) -> (d, SortColumn t)
|
||||||
|
sortField queryTerms = ("terms", SortColumn $ queryTerms >>> (E.?. StudyTermsName))
|
||||||
|
|
||||||
|
fltrField :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool))
|
||||||
|
, IsString d
|
||||||
|
)
|
||||||
|
=> (a -> E.SqlExpr (Maybe (Entity StudyTerms)))
|
||||||
|
-> (d, FilterColumn t)
|
||||||
|
fltrField queryFeatures = ( "terms"
|
||||||
|
, FilterColumn $ anyFilter
|
||||||
|
[ mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyTermsName)
|
||||||
|
, mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyTermsShorthand)
|
||||||
|
, mkExactFilterWith readMay $ queryFeatures >>> (E.?. StudyTermsKey)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
fltrFieldUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
|
fltrFieldUI mPrev =
|
||||||
|
prismAForm (singletonFilter "terms") mPrev $ aopt textField (fslI MsgStudyTerm)
|
||||||
|
|
||||||
|
|
||||||
|
colDegreeShort :: (IsDBTable m c, HasStudyDegree x) => Getting (Leftmost x) a x -> Colonnade Sortable a (DBCell m c)
|
||||||
|
colDegreeShort terms = sortable (Just "degree-short") (i18nCell MsgDegreeShort) $ maybe mempty cellHasDegreeShort . firstOf terms
|
||||||
|
|
||||||
|
sortDegreeShort :: IsString d => (t -> E.SqlExpr (Maybe (Entity StudyDegree))) -> (d, SortColumn t)
|
||||||
|
sortDegreeShort queryTerms = ("degree-short", SortColumn $ queryTerms >>> (E.?. StudyDegreeShorthand))
|
||||||
|
|
||||||
|
fltrDegree :: ( IsFilterColumn t (a -> Set Text -> E.SqlExpr (E.Value Bool))
|
||||||
|
, IsString d
|
||||||
|
)
|
||||||
|
=> (a -> E.SqlExpr (Maybe (Entity StudyDegree)))
|
||||||
|
-> (d, FilterColumn t)
|
||||||
|
fltrDegree queryFeatures = ( "degree"
|
||||||
|
, FilterColumn $ anyFilter
|
||||||
|
[ mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyDegreeName)
|
||||||
|
, mkContainsFilterWith Just $ queryFeatures >>> E.joinV . (E.?. StudyDegreeShorthand)
|
||||||
|
, mkExactFilterWith readMay $ queryFeatures >>> (E.?. StudyDegreeKey)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
fltrDegreeUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
|
fltrDegreeUI mPrev =
|
||||||
|
prismAForm (singletonFilter "degree") mPrev $ aopt textField (fslI MsgDegreeName)
|
||||||
|
|||||||
@ -6,7 +6,8 @@ module Handler.Utils.Table.Pagination
|
|||||||
, FilterColumn(..), IsFilterColumn
|
, FilterColumn(..), IsFilterColumn
|
||||||
, DBRow(..), _dbrOutput, _dbrIndex, _dbrCount
|
, DBRow(..), _dbrOutput, _dbrIndex, _dbrCount
|
||||||
, DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..)
|
, DBStyle(..), defaultDBSFilterLayout, DBEmptyStyle(..)
|
||||||
, DBTable(..), IsDBTable(..), DBCell(..)
|
, DBTCsvEncode, DBTCsvDecode
|
||||||
|
, DBTable(..), noCsvEncode, IsDBTable(..), DBCell(..)
|
||||||
, singletonFilter
|
, singletonFilter
|
||||||
, DBParams(..)
|
, DBParams(..)
|
||||||
, cellAttrs, cellContents
|
, cellAttrs, cellContents
|
||||||
@ -35,6 +36,8 @@ module Handler.Utils.Table.Pagination
|
|||||||
import Handler.Utils.Table.Pagination.Types
|
import Handler.Utils.Table.Pagination.Types
|
||||||
import Handler.Utils.Table.Pagination.Utils (getTableWidget)
|
import Handler.Utils.Table.Pagination.Utils (getTableWidget)
|
||||||
import Handler.Utils.Form
|
import Handler.Utils.Form
|
||||||
|
import Handler.Utils.Csv
|
||||||
|
import Handler.Utils.ContentDisposition
|
||||||
import Utils
|
import Utils
|
||||||
import Utils.Lens.TH
|
import Utils.Lens.TH
|
||||||
|
|
||||||
@ -68,7 +71,8 @@ import Text.Hamlet (hamletFile)
|
|||||||
|
|
||||||
import Data.Ratio ((%))
|
import Data.Ratio ((%))
|
||||||
|
|
||||||
import Control.Lens
|
import Control.Lens hiding ((<.>))
|
||||||
|
import Control.Lens.Extras (is)
|
||||||
|
|
||||||
import Data.List (elemIndex)
|
import Data.List (elemIndex)
|
||||||
|
|
||||||
@ -90,6 +94,8 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
|
|
||||||
import Data.Semigroup as Sem (Semigroup(..))
|
import Data.Semigroup as Sem (Semigroup(..))
|
||||||
|
|
||||||
|
import qualified Data.Conduit.List as C
|
||||||
|
|
||||||
|
|
||||||
#if MIN_VERSION_base(4,11,0)
|
#if MIN_VERSION_base(4,11,0)
|
||||||
type Monoid' = Monoid
|
type Monoid' = Monoid
|
||||||
@ -155,12 +161,12 @@ instance IsFilterColumn t (E.SqlExpr (E.Value Bool)) where
|
|||||||
filterColumn' fin _ _ = fin
|
filterColumn' fin _ _ = fin
|
||||||
|
|
||||||
instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
|
instance IsFilterColumn t cont => IsFilterColumn t (t -> cont) where
|
||||||
filterColumn' cont is t = filterColumn' (cont t) is t
|
filterColumn' cont is' t = filterColumn' (cont t) is' t
|
||||||
|
|
||||||
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
|
instance {-# OVERLAPPABLE #-} (PathPiece (Element l), IsFilterColumn t cont, MonoPointed l, Monoid l) => IsFilterColumn t (l -> cont) where
|
||||||
filterColumn' cont is = filterColumn' (cont input) is'
|
filterColumn' cont is0 = filterColumn' (cont input) is'
|
||||||
where
|
where
|
||||||
(input, ($ []) -> is') = go (mempty, id) is
|
(input, ($ []) -> is') = go (mempty, id) is0
|
||||||
go acc [] = acc
|
go acc [] = acc
|
||||||
go (acc, is3) (i:is2)
|
go (acc, is3) (i:is2)
|
||||||
| Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2
|
| Just i' <- fromPathPiece i = go (acc `mappend` singleton i', is3) is2
|
||||||
@ -264,6 +270,37 @@ piIsUnset PaginationInput{..} = and
|
|||||||
, isNothing piPage
|
, isNothing piPage
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
data ButtonCsvMode = BtnCsvExport | BtnCsvImport
|
||||||
|
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
|
||||||
|
instance Universe ButtonCsvMode
|
||||||
|
instance Finite ButtonCsvMode
|
||||||
|
|
||||||
|
embedRenderMessage ''UniWorX ''ButtonCsvMode id
|
||||||
|
|
||||||
|
nullaryPathPiece ''ButtonCsvMode $ camelToPathPiece' 1
|
||||||
|
|
||||||
|
instance Button UniWorX ButtonCsvMode where
|
||||||
|
btnLabel BtnCsvExport
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
#{iconCSV}
|
||||||
|
\ _{BtnCsvExport}
|
||||||
|
|]
|
||||||
|
btnLabel BtnCsvImport
|
||||||
|
= [whamlet|
|
||||||
|
$newline never
|
||||||
|
_{BtnCsvImport}
|
||||||
|
|]
|
||||||
|
|
||||||
|
data DBCsvMode = DBCsvNormal
|
||||||
|
| DBCsvExport
|
||||||
|
| DBCsvImport
|
||||||
|
{ _dbCsvFiles :: [FileInfo]
|
||||||
|
, _dbCsvModifyExisting, _dbCsvAddNew, _dbCsvDeleteMissing :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k')
|
type DBTableKey k' = (ToJSON k', FromJSON k', Ord k', Binary k')
|
||||||
data DBRow r = forall k'. DBTableKey k' => DBRow
|
data DBRow r = forall k'. DBTableKey k' => DBRow
|
||||||
{ dbrKey :: k'
|
{ dbrKey :: k'
|
||||||
@ -405,7 +442,10 @@ instance PathPiece x => PathPiece (WithIdent x) where
|
|||||||
WithIdent <$> pure ident <*> fromPathPiece rest
|
WithIdent <$> pure ident <*> fromPathPiece rest
|
||||||
|
|
||||||
|
|
||||||
data DBTable m x = forall a r r' h i t k k'.
|
type DBTCsvEncode r' csv = DictMaybe (ToNamedRecord csv, DefaultOrdered csv) (Conduit r' (YesodDB UniWorX) csv)
|
||||||
|
type DBTCsvDecode csv = DictMaybe (FromNamedRecord csv) (Sink csv (YesodDB UniWorX) ())
|
||||||
|
|
||||||
|
data DBTable m x = forall a r r' h i t k k' csv.
|
||||||
( ToSortable h, Functor h
|
( ToSortable h, Functor h
|
||||||
, E.SqlSelect a r, E.SqlIn k k', DBTableKey k'
|
, E.SqlSelect a r, E.SqlIn k k', DBTableKey k'
|
||||||
, PathPiece i, Eq i
|
, PathPiece i, Eq i
|
||||||
@ -413,16 +453,21 @@ data DBTable m x = forall a r r' h i t k k'.
|
|||||||
) => DBTable
|
) => DBTable
|
||||||
{ dbtSQLQuery :: t -> E.SqlQuery a
|
{ dbtSQLQuery :: t -> E.SqlQuery a
|
||||||
, dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples.
|
, dbtRowKey :: t -> k -- ^ required for table forms; always same key for repeated requests. For joins: return unique tuples.
|
||||||
, dbtProj :: DBRow r -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) r'
|
, dbtProj :: DBRow r -> MaybeT (YesodDB UniWorX) r'
|
||||||
, dbtColonnade :: Colonnade h r' (DBCell m x)
|
, dbtColonnade :: Colonnade h r' (DBCell m x)
|
||||||
, dbtSorting :: Map SortingKey (SortColumn t)
|
, dbtSorting :: Map SortingKey (SortColumn t)
|
||||||
, dbtFilter :: Map FilterKey (FilterColumn t)
|
, dbtFilter :: Map FilterKey (FilterColumn t)
|
||||||
, dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (ReaderT SqlBackend (HandlerT UniWorX IO)) (Map FilterKey [Text])
|
, dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
|
||||||
, dbtStyle :: DBStyle
|
, dbtStyle :: DBStyle
|
||||||
, dbtParams :: DBParams m x
|
, dbtParams :: DBParams m x
|
||||||
|
, dbtCsvEncode :: DBTCsvEncode r' csv
|
||||||
|
, dbtCsvDecode :: DBTCsvDecode csv
|
||||||
, dbtIdent :: i
|
, dbtIdent :: i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
noCsvEncode :: DictMaybe (ToNamedRecord Void, DefaultOrdered Void) (Conduit r' (YesodDB UniWorX) Void)
|
||||||
|
noCsvEncode = Nothing
|
||||||
|
|
||||||
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where
|
class (MonadHandler m, HandlerSite m ~ UniWorX, Monoid' x, Monoid' (DBCell m x), Default (DBParams m x)) => IsDBTable (m :: * -> *) (x :: *) where
|
||||||
data DBParams m x :: *
|
data DBParams m x :: *
|
||||||
type DBResult m x :: *
|
type DBResult m x :: *
|
||||||
@ -694,18 +739,68 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
|
|
||||||
mapM_ (addMessageI Warning) errs
|
mapM_ (addMessageI Warning) errs
|
||||||
|
|
||||||
|
Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler
|
||||||
|
getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
|
||||||
|
let
|
||||||
|
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX
|
||||||
|
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams
|
||||||
|
substPi = foldr (.) id
|
||||||
|
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting
|
||||||
|
, foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter
|
||||||
|
, setParam (wIdent "pagesize") $ fmap toPathPiece piLimit
|
||||||
|
, setParam (wIdent "page") $ fmap toPathPiece piPage
|
||||||
|
, setParam (wIdent "pagination") Nothing
|
||||||
|
]
|
||||||
|
tblLink' :: (QueryText -> QueryText) -> Widget
|
||||||
|
tblLink' = toWidget <=< toTextUrl . tblLink
|
||||||
|
|
||||||
|
((csvExportRes, csvExportWdgt), csvExportEnctype) <- lift . runFormGet . identifyForm FIDDBTableCsvExport . set (mapped . mapped . _1 . mapped) DBCsvExport $ buttonForm' [BtnCsvExport]
|
||||||
|
((csvImportRes, csvImportWdgt), csvImportEnctype) <- lift . runFormPost . identifyForm FIDDBTableCsvImport . renderAForm FormDBTableCsvImport $ DBCsvImport
|
||||||
|
<$> areq fileFieldMultiple (fslI MsgCsvFile) Nothing
|
||||||
|
<*> apopt checkBoxField (fslI MsgCsvModifyExisting) (Just True)
|
||||||
|
<*> apopt checkBoxField (fslI MsgCsvAddNew) (Just True)
|
||||||
|
<*> apopt checkBoxField (fslI MsgCsvDeleteMissing) (Just False)
|
||||||
|
|
||||||
|
let
|
||||||
|
csvMode = asum
|
||||||
|
[ csvExportRes <* guard (is _Just dbtCsvEncode)
|
||||||
|
, csvImportRes <* guard (is _Just dbtCsvDecode)
|
||||||
|
, FormSuccess DBCsvNormal
|
||||||
|
]
|
||||||
|
csvExportWdgt' = wrapForm csvExportWdgt FormSettings
|
||||||
|
{ formMethod = GET
|
||||||
|
, formAction = Just $ tblLink id
|
||||||
|
, formEncoding = csvExportEnctype
|
||||||
|
, formAttrs = [("target", "_blank")]
|
||||||
|
, formSubmit = FormNoSubmit
|
||||||
|
, formAnchor = Nothing :: Maybe Text
|
||||||
|
}
|
||||||
|
csvImportWdgt' = wrapForm' BtnCsvImport csvImportWdgt FormSettings
|
||||||
|
{ formMethod = POST
|
||||||
|
, formAction = Just $ tblLink id
|
||||||
|
, formEncoding = csvImportEnctype
|
||||||
|
, formAttrs = []
|
||||||
|
, formSubmit = FormSubmit
|
||||||
|
, formAnchor = Nothing :: Maybe Text
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
rows' <- E.select . E.from $ \t -> do
|
rows' <- E.select . E.from $ \t -> do
|
||||||
res <- dbtSQLQuery t
|
res <- dbtSQLQuery t
|
||||||
E.orderBy (map (sqlSortDirection t) psSorting')
|
E.orderBy (map (sqlSortDirection t) psSorting')
|
||||||
case previousKeys of
|
case csvMode of
|
||||||
Nothing
|
FormSuccess DBCsvExport -> return ()
|
||||||
| PagesizeLimit l <- psLimit
|
FormSuccess DBCsvImport{} -> return ()
|
||||||
-> do
|
_other -> do
|
||||||
E.limit l
|
case previousKeys of
|
||||||
E.offset (psPage * l)
|
Nothing
|
||||||
Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps
|
| PagesizeLimit l <- psLimit
|
||||||
_other -> return ()
|
-> do
|
||||||
Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter
|
E.limit l
|
||||||
|
E.offset (psPage * l)
|
||||||
|
Just ps -> E.where_ $ dbtRowKey t `E.sqlIn` ps
|
||||||
|
_other -> return ()
|
||||||
|
Map.foldrWithKey (\key args expr -> E.where_ (filterColumn (Map.findWithDefault (error $ "Invalid filter key: " <> show key) key dbtFilter) args t) >> expr) (return ()) psFilter
|
||||||
return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res)
|
return (E.unsafeSqlValue "count(*) OVER ()" :: E.SqlExpr (E.Value Int64), dbtRowKey t, res)
|
||||||
|
|
||||||
let mapMaybeM f = fmap catMaybes . mapM (\(k, v) -> runMaybeT $ (,) <$> pure k <*> f v)
|
let mapMaybeM f = fmap catMaybes . mapM (\(k, v) -> runMaybeT $ (,) <$> pure k <*> f v)
|
||||||
@ -723,20 +818,17 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
|
|
||||||
(currentKeys, rows) <- fmap unzip . mapMaybeM dbtProj . map (\(dbrIndex, (E.Value dbrCount, dbrKey, dbrOutput)) -> (dbrKey, DBRow{..})) . zip [firstRow..] $ reproduceSorting rows'
|
(currentKeys, rows) <- fmap unzip . mapMaybeM dbtProj . map (\(dbrIndex, (E.Value dbrCount, dbrKey, dbrOutput)) -> (dbrKey, DBRow{..})) . zip [firstRow..] $ reproduceSorting rows'
|
||||||
|
|
||||||
Just currentRoute <- getCurrentRoute -- `dbTable` should never be called from a 404-handler
|
|
||||||
getParams <- liftHandlerT $ queryToQueryText . Wai.queryString . reqWaiRequest <$> getRequest
|
formResult csvMode $ \case
|
||||||
let
|
DBCsvExport
|
||||||
tblLink :: (QueryText -> QueryText) -> SomeRoute UniWorX
|
| Just (Dict, dbtCsvEncode') <- dbtCsvEncode
|
||||||
tblLink f = SomeRoute . (currentRoute, ) . over (mapped . _2) (fromMaybe Text.empty) $ (f . substPi . setParam "_hasdata" Nothing) getParams
|
-> do
|
||||||
substPi = foldr (.) id
|
setContentDisposition' . Just $ unpack dbtIdent <.> unpack extensionCsv
|
||||||
[ setParams (wIdent "sorting") . map toPathPiece $ fromMaybe [] piSorting
|
sendResponse <=< liftHandlerT . respondCsvDB $ C.sourceList rows .| dbtCsvEncode'
|
||||||
, foldr (.) id . map (\k -> setParams (wIdent $ toPathPiece k) . fromMaybe [] . join $ traverse (Map.lookup k) piFilter) $ Map.keys dbtFilter
|
DBCsvImport{}
|
||||||
, setParam (wIdent "pagesize") $ fmap toPathPiece piLimit
|
| Just (Dict, _dbtCsvDecode) <- dbtCsvDecode
|
||||||
, setParam (wIdent "page") $ fmap toPathPiece piPage
|
-> error "dbCsvImport"
|
||||||
, setParam (wIdent "pagination") Nothing
|
_other -> return ()
|
||||||
]
|
|
||||||
tblLink' :: (QueryText -> QueryText) -> Widget
|
|
||||||
tblLink' = toWidget <=< toTextUrl . tblLink
|
|
||||||
|
|
||||||
let
|
let
|
||||||
rowCount
|
rowCount
|
||||||
@ -791,6 +883,9 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
, formSubmit = FormAutoSubmit
|
, formSubmit = FormAutoSubmit
|
||||||
, formAnchor = Just $ wIdent "pagesize-form"
|
, formAnchor = Just $ wIdent "pagesize-form"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
csvWdgt = $(widgetFile "table/csv-transcode")
|
||||||
|
|
||||||
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
|
uiLayout table = dbsFilterLayout filterWdgt filterEnc (SomeRoute $ rawAction :#: wIdent "table-wrapper") $(widgetFile "table/layout")
|
||||||
|
|
||||||
dbInvalidateResult' = foldr (<=<) return . catMaybes $
|
dbInvalidateResult' = foldr (<=<) return . catMaybes $
|
||||||
|
|||||||
@ -74,6 +74,9 @@ import Network.Mime as Import
|
|||||||
import Data.Aeson.TH as Import
|
import Data.Aeson.TH as Import
|
||||||
import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value)
|
import Data.Aeson.Types as Import (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), toJSONKeyText, FromJSONKeyFunction(..), ToJSONKeyFunction(..), Value)
|
||||||
|
|
||||||
|
import Data.Constraint as Import (Dict(..))
|
||||||
|
import Data.Void as Import (Void)
|
||||||
|
|
||||||
import Language.Haskell.TH.Instances as Import ()
|
import Language.Haskell.TH.Instances as Import ()
|
||||||
import Data.List.NonEmpty.Instances as Import ()
|
import Data.List.NonEmpty.Instances as Import ()
|
||||||
import Data.NonNull.Instances as Import ()
|
import Data.NonNull.Instances as Import ()
|
||||||
|
|||||||
15
src/Utils.hs
15
src/Utils.hs
@ -77,6 +77,8 @@ import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
|||||||
|
|
||||||
import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice)
|
import Algebra.Lattice (top, bottom, (/\), (\/), BoundedJoinSemiLattice, BoundedMeetSemiLattice)
|
||||||
|
|
||||||
|
import Data.Constraint (Dict(..))
|
||||||
|
|
||||||
{-# ANN choice ("HLint: ignore Use asum" :: String) #-}
|
{-# ANN choice ("HLint: ignore Use asum" :: String) #-}
|
||||||
|
|
||||||
|
|
||||||
@ -164,6 +166,10 @@ fileDownload = fontAwesomeIcon "file-download"
|
|||||||
zipDownload :: Markup
|
zipDownload :: Markup
|
||||||
zipDownload = fontAwesomeIcon "file-archive"
|
zipDownload = fontAwesomeIcon "file-archive"
|
||||||
|
|
||||||
|
iconCSV :: Markup
|
||||||
|
iconCSV = fontAwesomeIcon "file-csv"
|
||||||
|
|
||||||
|
|
||||||
-- Conditional icons
|
-- Conditional icons
|
||||||
|
|
||||||
isVisible :: Bool -> Markup
|
isVisible :: Bool -> Markup
|
||||||
@ -983,3 +989,12 @@ foldJoin = foldr (\/) bottom
|
|||||||
|
|
||||||
foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono
|
foldMeet :: (MonoFoldable mono, BoundedMeetSemiLattice (Element mono)) => mono -> Element mono
|
||||||
foldMeet = foldr (/\) top
|
foldMeet = foldr (/\) top
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- Constraints --
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
type DictMaybe constr a = Maybe (Dict constr, a)
|
||||||
|
|
||||||
|
pattern DictJust :: constr => a -> DictMaybe constr a
|
||||||
|
pattern DictJust a = Just (Dict, a)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import Yesod.Core.Instances ()
|
|||||||
import Settings
|
import Settings
|
||||||
|
|
||||||
import Utils.Parameters
|
import Utils.Parameters
|
||||||
|
import Utils.Lens
|
||||||
|
|
||||||
import Text.Blaze (Markup)
|
import Text.Blaze (Markup)
|
||||||
import qualified Text.Blaze.Internal as Blaze (null)
|
import qualified Text.Blaze.Internal as Blaze (null)
|
||||||
@ -32,8 +33,6 @@ import Control.Monad.Morph (MFunctor(..))
|
|||||||
|
|
||||||
import Data.List ((!!))
|
import Data.List ((!!))
|
||||||
|
|
||||||
import Control.Lens
|
|
||||||
|
|
||||||
import Web.PathPieces
|
import Web.PathPieces
|
||||||
|
|
||||||
import Data.UUID
|
import Data.UUID
|
||||||
@ -197,6 +196,8 @@ data FormIdentifier
|
|||||||
| FIDDBTableFilter
|
| FIDDBTableFilter
|
||||||
| FIDDBTablePagesize
|
| FIDDBTablePagesize
|
||||||
| FIDDBTable
|
| FIDDBTable
|
||||||
|
| FIDDBTableCsvExport
|
||||||
|
| FIDDBTableCsvImport
|
||||||
| FIDDelete
|
| FIDDelete
|
||||||
| FIDCourseRegister
|
| FIDCourseRegister
|
||||||
| FIDuserRights
|
| FIDuserRights
|
||||||
@ -591,6 +592,19 @@ htmlFieldSmall = checkMMap sanitize (pack . renderHtml) textField
|
|||||||
sanitize :: Text -> m (Either FormMessage Html)
|
sanitize :: Text -> m (Either FormMessage Html)
|
||||||
sanitize = return . Right . preEscapedText . sanitizeBalance
|
sanitize = return . Right . preEscapedText . sanitizeBalance
|
||||||
|
|
||||||
|
fileFieldMultiple :: Monad m => Field m [FileInfo]
|
||||||
|
fileFieldMultiple = Field
|
||||||
|
{ fieldParse = \_ files -> return $ case files of
|
||||||
|
[] -> Right Nothing
|
||||||
|
fs -> Right $ Just fs
|
||||||
|
, fieldView = \id' name attrs _ isReq ->
|
||||||
|
[whamlet|
|
||||||
|
$newline never
|
||||||
|
<input type="file" uw-file-input id=#{id'} name=#{name} *{attrs} multiple :isReq:required="required">
|
||||||
|
|]
|
||||||
|
, fieldEnctype = Multipart
|
||||||
|
}
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
-- Forms --
|
-- Forms --
|
||||||
-----------
|
-----------
|
||||||
@ -635,7 +649,7 @@ wrapForm' btn formWidget FormSettings{..} = do
|
|||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
-- | Use this type to pass information to the form template
|
-- | Use this type to pass information to the form template
|
||||||
data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize
|
data FormLayout = FormStandard | FormDBTableFilter | FormDBTablePagesize | FormDBTableCsvImport
|
||||||
|
|
||||||
renderAForm :: Monad m => FormLayout -> FormRender m a
|
renderAForm :: Monad m => FormLayout -> FormRender m a
|
||||||
renderAForm formLayout aform fragment = do
|
renderAForm formLayout aform fragment = do
|
||||||
@ -932,3 +946,20 @@ apreq f fs mx = formToAForm $ over _2 pure <$> mpreq f fs mx
|
|||||||
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
wpreq :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
|
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
|
||||||
wpreq f fs mx = mFormToWForm $ mpreq f fs mx
|
wpreq f fs mx = mFormToWForm $ mpreq f fs mx
|
||||||
|
|
||||||
|
|
||||||
|
mpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
|
=> Field m a -> FieldSettings site -> Maybe a -> MForm m (FormResult a, FieldView site)
|
||||||
|
-- ^ Pseudo optional
|
||||||
|
--
|
||||||
|
-- `FieldView` has `fvRequired` set to `False`
|
||||||
|
-- Otherwise acts exactly like `mreq`.
|
||||||
|
mpopt f fs mx = set (_2 . _fvRequired) False <$> mreq f fs mx
|
||||||
|
|
||||||
|
apopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
|
=> Field m a -> FieldSettings site -> Maybe a -> AForm m a
|
||||||
|
apopt f fs mx = formToAForm $ over _2 pure <$> mpopt f fs mx
|
||||||
|
|
||||||
|
wpopt :: (RenderMessage site FormMessage, HandlerSite m ~ site, MonadHandler m)
|
||||||
|
=> Field m a -> FieldSettings site -> Maybe a -> WForm m (FormResult a)
|
||||||
|
wpopt f fs mx = mFormToWForm $ mpopt f fs mx
|
||||||
|
|||||||
@ -41,13 +41,13 @@ _nullable = prism' toNullable fromNullable
|
|||||||
|
|
||||||
|
|
||||||
-- makeLenses_ ''Course
|
-- makeLenses_ ''Course
|
||||||
makeClassyFor_ "HasCourse" "hasCourse" ''Course
|
makeClassyFor_ ''Course
|
||||||
-- class HasCourse c where
|
-- class HasCourse c where
|
||||||
-- hasCourse :: Lens' c Course
|
-- hasCourse :: Lens' c Course
|
||||||
|
|
||||||
|
|
||||||
-- makeLenses_ ''User
|
-- makeLenses_ ''User
|
||||||
makeClassyFor_ "HasUser" "hasUser" ''User
|
makeClassyFor_ ''User
|
||||||
-- > :info HasUser
|
-- > :info HasUser
|
||||||
-- class HasUser c where
|
-- class HasUser c where
|
||||||
-- hasUser :: Lens' c User -- MINIMAL
|
-- hasUser :: Lens' c User -- MINIMAL
|
||||||
@ -56,8 +56,24 @@ makeClassyFor_ "HasUser" "hasUser" ''User
|
|||||||
-- _user...
|
-- _user...
|
||||||
--
|
--
|
||||||
|
|
||||||
|
makeClassyFor_ ''StudyFeatures
|
||||||
|
|
||||||
|
makeClassyFor_ ''StudyDegree
|
||||||
|
|
||||||
|
makeClassyFor_ ''StudyTerms
|
||||||
|
|
||||||
|
|
||||||
makeLenses_ ''Entity
|
makeLenses_ ''Entity
|
||||||
|
|
||||||
|
instance HasStudyFeatures a => HasStudyFeatures (Entity a) where
|
||||||
|
hasStudyFeatures = _entityVal . hasStudyFeatures
|
||||||
|
|
||||||
|
instance HasStudyTerms a => HasStudyTerms (Entity a) where
|
||||||
|
hasStudyTerms = _entityVal . hasStudyTerms
|
||||||
|
|
||||||
|
instance HasStudyDegree a => HasStudyDegree (Entity a) where
|
||||||
|
hasStudyDegree = _entityVal . hasStudyDegree
|
||||||
|
|
||||||
-- BUILD SERVER FAILS TO MAKE HADDOCK FOR THE ONE BELOW:
|
-- BUILD SERVER FAILS TO MAKE HADDOCK FOR THE ONE BELOW:
|
||||||
-- makeClassyFor_ "HasEntity" "hasEntity" ''Entity
|
-- makeClassyFor_ "HasEntity" "hasEntity" ''Entity
|
||||||
-- class HasEntity c record | c -> record where
|
-- class HasEntity c record | c -> record where
|
||||||
@ -96,12 +112,6 @@ makePrisms ''AuthResult
|
|||||||
|
|
||||||
makePrisms ''FormResult
|
makePrisms ''FormResult
|
||||||
|
|
||||||
makeLenses_ ''StudyFeatures
|
|
||||||
|
|
||||||
makeLenses_ ''StudyDegree
|
|
||||||
|
|
||||||
makeLenses_ ''StudyTerms
|
|
||||||
|
|
||||||
makeLenses_ ''StudyTermCandidate
|
makeLenses_ ''StudyTermCandidate
|
||||||
|
|
||||||
makeLenses_ ''FieldView
|
makeLenses_ ''FieldView
|
||||||
@ -133,6 +143,8 @@ makeLenses_ ''ExamGradingRule
|
|||||||
|
|
||||||
makeLenses_ ''UTCTime
|
makeLenses_ ''UTCTime
|
||||||
|
|
||||||
|
makeLenses_ ''ExamOccurrence
|
||||||
|
|
||||||
|
|
||||||
-- makeClassy_ ''Load
|
-- makeClassy_ ''Load
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
module Utils.Lens.TH where
|
module Utils.Lens.TH where
|
||||||
|
|
||||||
import ClassyPrelude (String, Maybe(..))
|
import ClassyPrelude (Maybe(..), (<>))
|
||||||
import Control.Lens
|
import Control.Lens
|
||||||
import Control.Lens.Internal.FieldTH
|
import Control.Lens.Internal.FieldTH
|
||||||
import Language.Haskell.TH
|
import Language.Haskell.TH
|
||||||
@ -56,9 +56,12 @@ makeLenses_ = makeFieldOptics lensRules_
|
|||||||
|
|
||||||
-- | like makeClassyFor but only specifies names for class and its function,
|
-- | like makeClassyFor but only specifies names for class and its function,
|
||||||
-- otherwise lenses are created with underscore like `makeLenses_`
|
-- otherwise lenses are created with underscore like `makeLenses_`
|
||||||
makeClassyFor_ :: String -> String -> Name -> DecsQ
|
makeClassyFor_ :: Name -> DecsQ
|
||||||
makeClassyFor_ clsName funName = makeFieldOptics (classyRulesFor_ clNamer)
|
makeClassyFor_ recName = makeFieldOptics (classyRulesFor_ clNamer) recName
|
||||||
where
|
where
|
||||||
clNamer :: ClassyNamer
|
clsName = "Has" <> nameBase recName
|
||||||
-- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17
|
funName = "has" <> nameBase recName
|
||||||
clNamer _ = Just (mkName clsName, mkName funName)
|
|
||||||
|
clNamer :: ClassyNamer
|
||||||
|
-- clNamer _ = Just (clsName, funName) -- for newer versions >= 4.17
|
||||||
|
clNamer _ = Just (mkName clsName, mkName funName)
|
||||||
|
|||||||
4
start.sh
4
start.sh
@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
unset HOST
|
unset HOST
|
||||||
export DETAILED_LOGGING=${DETAILED_LOGGIN:-true}
|
export DETAILED_LOGGING=${DETAILED_LOGGIN:-true}
|
||||||
export LOG_ALL=${LOG_ALL:-false}
|
export LOG_ALL=${LOG_ALL:-false}
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 820 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +1,8 @@
|
|||||||
/*!
|
/*!
|
||||||
* Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
|
* Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com
|
||||||
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
* License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||||
*/
|
*/
|
||||||
@font-face{
|
@font-face{
|
||||||
font-family:"Font Awesome 5 Free";
|
font-family:"Font Awesome 5 Free";
|
||||||
font-style:normal;
|
font-style:normal;
|
||||||
font-weight:900;
|
font-weight:900;
|
||||||
|
|||||||
@ -130,6 +130,16 @@
|
|||||||
<td .table__th>#{getLoadSum shn}
|
<td .table__th>#{getLoadSum shn}
|
||||||
<td .table__th>#{ciSubmissions}
|
<td .table__th>#{ciSubmissions}
|
||||||
<td .table__td colspan=3>^{simpleLinkI (SomeMessage MsgMenuCorrectorsChange) (CSheetR tid ssh csh shn SCorrR)}
|
<td .table__td colspan=3>^{simpleLinkI (SomeMessage MsgMenuCorrectorsChange) (CSheetR tid ssh csh shn SCorrR)}
|
||||||
|
|
||||||
|
<tr .table__row .table__row--head>
|
||||||
|
<th>
|
||||||
|
<th colspan=2>
|
||||||
|
<th>
|
||||||
|
<th colspan=3>
|
||||||
|
$# Always iterate over orderedSheetNames for consistent sorting! Newest first, except in this table
|
||||||
|
$forall shn <- orderedSheetNames
|
||||||
|
<th .table__th colspan=5>#{shn}
|
||||||
|
|
||||||
^{btnWdgt}
|
^{btnWdgt}
|
||||||
<div>
|
<div>
|
||||||
<p>_{MsgAssignSubmissionsRandomWarning}
|
<p>_{MsgAssignSubmissionsRandomWarning}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
$newline never
|
$newline never
|
||||||
<p>
|
<p>
|
||||||
Stand: Mai 2019
|
Stand: July 2019
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>
|
||||||
|
Benachrichtigungen per eMail treffen manchmal mit mehreren Tagen Verzögerung ein.
|
||||||
<li>
|
<li>
|
||||||
Format von Bewertungsdateien ist noch provisorisch
|
Format von Bewertungsdateien ist noch provisorisch
|
||||||
|
|||||||
7
templates/table/csv-transcode.hamlet
Normal file
7
templates/table/csv-transcode.hamlet
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
$newline never
|
||||||
|
$if is _Just dbtCsvDecode
|
||||||
|
<div .csv-import>
|
||||||
|
^{csvImportWdgt'}
|
||||||
|
$if is _Just dbtCsvEncode
|
||||||
|
<div .csv-export>
|
||||||
|
^{csvExportWdgt'}
|
||||||
@ -5,6 +5,7 @@ $else
|
|||||||
<div .table-header>
|
<div .table-header>
|
||||||
<div .table__row-count>
|
<div .table__row-count>
|
||||||
_{MsgRowCount rowCount}
|
_{MsgRowCount rowCount}
|
||||||
|
^{csvWdgt}
|
||||||
|
|
||||||
^{table}
|
^{table}
|
||||||
|
|
||||||
|
|||||||
4
test.sh
4
test.sh
@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en .stack-work.lock "$0" "$@" || :
|
||||||
|
|
||||||
move-back() {
|
move-back() {
|
||||||
mv -v .stack-work .stack-work-test
|
mv -v .stack-work .stack-work-test
|
||||||
[[ -d .stack-work-build ]] && mv -v .stack-work-build .stack-work
|
[[ -d .stack-work-build ]] && mv -v .stack-work-build .stack-work
|
||||||
|
|||||||
Reference in New Issue
Block a user