Merge branch 'master' into feat/asynchronous-mass-input

This commit is contained in:
Felix Hamann 2019-04-25 16:38:16 +02:00
commit 763499f9e3
19 changed files with 217 additions and 41 deletions

View File

@ -269,7 +269,7 @@ CorByProportionIncludingTutorial proportion@Rational: #{display proportion} Ante
CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium CorByProportionExcludingTutorial proportion@Rational: #{display proportion} Anteile + Tutorium
RowCount count@Int64: #{display count} #{pluralDE count "Eintrag" "Einträge"} nach Filter RowCount count@Int64: #{display count} #{pluralDE count "Eintrag" "Einträge"} nach Filter
DeleteRow: Zeile entfernen DeleteRow: Entfernen
ProportionNegative: Anteile dürfen nicht negativ sein ProportionNegative: Anteile dürfen nicht negativ sein
CorrectorUpdated: Korrektor erfolgreich aktualisiert CorrectorUpdated: Korrektor erfolgreich aktualisiert
CorrectorsUpdated: Korrektoren erfolgreich aktualisiert CorrectorsUpdated: Korrektoren erfolgreich aktualisiert
@ -775,6 +775,7 @@ CommSuccess n@Int: Nachricht wurde an #{tshow n} Empfänger versandt
CommCourseHeading: Kursmitteilung CommCourseHeading: Kursmitteilung
RecipientCustom: Weitere Empfänger RecipientCustom: Weitere Empfänger
RecipientToggleAll: Alle/Keine
RGCourseParticipants: Kursteilnehmer RGCourseParticipants: Kursteilnehmer
RGCourseLecturers: Kursverwalter RGCourseLecturers: Kursverwalter

View File

@ -121,6 +121,7 @@ dependencies:
- jose-jwt - jose-jwt
- mono-traversable - mono-traversable
- lens-aeson - lens-aeson
- systemd
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -24,7 +24,8 @@ import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware) import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings, import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException, defaultShouldDisplayException,
runSettings, setHost, runSettings, runSettingsSocket, setHost,
setBeforeMainLoop,
setOnException, setPort, getPort) setOnException, setPort, getPort)
import Network.Wai.Middleware.RequestLogger (Destination (Logger), import Network.Wai.Middleware.RequestLogger (Destination (Logger),
IPAddrSource (..), IPAddrSource (..),
@ -72,6 +73,8 @@ import System.Exit (exitFailure)
import qualified Database.Memcached.Binary.IO as Memcached import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd
-- Import all relevant handler modules here. -- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.) -- (HPack takes care to add new modules to our cabal file nowadays.)
import Handler.Common import Handler.Common
@ -291,6 +294,7 @@ warpSettings :: UniWorX -> Settings
warpSettings foundation = defaultSettings warpSettings foundation = defaultSettings
& setPort (foundation ^. _appPort) & setPort (foundation ^. _appPort)
& setHost (foundation ^. _appHost) & setHost (foundation ^. _appHost)
& setBeforeMainLoop (void Systemd.notifyReady)
& setOnException (\_req e -> & setOnException (\_req e ->
when (defaultShouldDisplayException e) $ do when (defaultShouldDisplayException e) $ do
logger <- readTVarIO . snd $ appLogger foundation logger <- readTVarIO . snd $ appLogger foundation
@ -338,7 +342,12 @@ appMain = runResourceT $ do
app <- makeApplication foundation app <- makeApplication foundation
-- Run the application with Warp -- Run the application with Warp
liftIO $ runSettings (warpSettings foundation) app activatedSockets <- liftIO Systemd.getActivatedSockets
liftIO $ case activatedSockets of
Just [sock]
-> runSettingsSocket (warpSettings foundation) sock app
_other
-> runSettings (warpSettings foundation) app
-------------------------------------------------------------- --------------------------------------------------------------

View File

@ -721,7 +721,9 @@ postCorrectionsUploadR = do
, formEncoding = uploadEncoding , formEncoding = uploadEncoding
} }
defaultLayout
defaultLayout $ do
let uploadInstruction = $(i18nWidgetFile "corrections-upload-instructions")
$(widgetFile "corrections-upload") $(widgetFile "corrections-upload")
getCorrectionsCreateR, postCorrectionsCreateR :: Handler Html getCorrectionsCreateR, postCorrectionsCreateR :: Handler Html

View File

@ -803,8 +803,9 @@ userTableQuery :: CourseId -> UserTableExpr -> E.SqlQuery ( E.SqlExpr (Entity Us
userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures) = do userTableQuery cid ((user `E.InnerJoin` participant) `E.LeftOuterJoin` note `E.LeftOuterJoin` studyFeatures) = do
-- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis -- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis
features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures
E.on $ E.just (participant E.^. CourseParticipantUser) E.==. note E.?. CourseUserNoteUser E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser))
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId E.&&. (note E.?. CourseUserNoteCourse E.==. E.just (E.val cid))
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid E.where_ $ participant E.^. CourseParticipantCourse E.==. E.val cid
return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features) return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features)

View File

@ -27,7 +27,7 @@ data SettingsForm = SettingsForm
} }
makeSettingForm :: Maybe SettingsForm -> Form SettingsForm makeSettingForm :: Maybe SettingsForm -> Form SettingsForm
makeSettingForm template = identifyForm FIDsettings $ \html -> do makeSettingForm template html = do
(result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm (result, widget) <- flip (renderAForm FormStandard) html $ SettingsForm
<$ aformSection MsgFormCosmetics <$ aformSection MsgFormCosmetics
<*> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here <*> areq (natFieldI $ MsgNatField "Favoriten") -- TODO: natFieldI not working here

View File

@ -727,6 +727,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
isSortable = isJust sortableKey isSortable = isJust sortableKey
isSorted = (`elem` directions) isSorted = (`elem` directions)
attrs = sortableContent ^. cellAttrs attrs = sortableContent ^. cellAttrs
piSorting' = [ sSet | sSet <- fromMaybe [] piSorting, Just (sortKey sSet) /= sortableKey ]
return $(widgetFile "table/cell/header") return $(widgetFile "table/cell/header")
columnCount :: Int64 columnCount :: Int64

View File

@ -16,7 +16,6 @@ import Utils
import Control.Lens hiding (universe) import Control.Lens hiding (universe)
import Utils.Lens.TH import Utils.Lens.TH
import Data.Map ((!))
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
@ -320,19 +319,16 @@ deriveJSON defaultOptions
} ''SubmissionMode } ''SubmissionMode
derivePersistFieldJSON ''SubmissionMode derivePersistFieldJSON ''SubmissionMode
instance PathPiece SubmissionMode where finitePathPiece ''SubmissionMode
toPathPiece = (Map.fromList (zip universeF verbs) !) [ "no-submissions"
where , "no-upload"
verbs = [ "no-submissions" , "no-unpack"
, "no-upload" , "unpack"
, "no-unpack" , "correctors"
, "unpack" , "correctors+no-upload"
, "correctors" , "correctors+no-unpack"
, "correctors+no-upload" , "correctors+unpack"
, "correctors+no-unpack" ]
, "correctors+unpack"
]
fromPathPiece = finiteFromPathPiece
data SubmissionModeDescr = SubmissionModeNone data SubmissionModeDescr = SubmissionModeNone
| SubmissionModeCorrector | SubmissionModeCorrector
@ -342,7 +338,12 @@ data SubmissionModeDescr = SubmissionModeNone
instance Universe SubmissionModeDescr instance Universe SubmissionModeDescr
instance Finite SubmissionModeDescr instance Finite SubmissionModeDescr
nullaryPathPiece ''SubmissionModeDescr $ camelToPathPiece' 2 finitePathPiece ''SubmissionModeDescr
[ "no-submissions"
, "correctors"
, "users"
, "correctors+users"
]
classifySubmissionMode :: SubmissionMode -> SubmissionModeDescr classifySubmissionMode :: SubmissionMode -> SubmissionModeDescr
classifySubmissionMode (SubmissionMode False Nothing ) = SubmissionModeNone classifySubmissionMode (SubmissionMode False Nothing ) = SubmissionModeNone

View File

@ -1,7 +1,7 @@
module Utils.PathPiece module Utils.PathPiece
( finiteFromPathPiece ( finiteFromPathPiece
, nullaryToPathPiece , nullaryToPathPiece
, nullaryPathPiece , nullaryPathPiece, finitePathPiece
, splitCamel , splitCamel
, camelToPathPiece, camelToPathPiece' , camelToPathPiece, camelToPathPiece'
, tuplePathPiece , tuplePathPiece
@ -16,6 +16,9 @@ import Data.Universe
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Char as Char import qualified Data.Char as Char
import Data.Map ((!), (!?))
import qualified Data.Map as Map
import Numeric.Natural import Numeric.Natural
import Data.List (foldl) import Data.List (foldl)
@ -45,6 +48,16 @@ nullaryPathPiece nullaryType mangle =
[ clause [] (normalB [e|finiteFromPathPiece|]) [] ] [ clause [] (normalB [e|finiteFromPathPiece|]) [] ]
] ]
finitePathPiece :: Name -> [Text] -> DecsQ
finitePathPiece finiteType verbs =
pure <$> instanceD (cxt []) [t|PathPiece $(conT finiteType)|]
[ funD 'toPathPiece
[ clause [] (normalB [|(Map.fromList (zip universeF verbs) !)|]) [] ]
, funD 'fromPathPiece
[ clause [] (normalB [e|(Map.fromList (zip verbs universeF) !?)|]) [] ]
]
splitCamel :: Textual t => t -> [t] splitCamel :: Textual t => t -> [t]
splitCamel = map fromList . reverse . helper (error "hasChange undefined at start of string") [] "" . otoList splitCamel = map fromList . reverse . helper (error "hasChange undefined at start of string") [] "" . otoList
where where

View File

@ -49,4 +49,6 @@ extra-deps:
- quickcheck-classes-0.4.14 - quickcheck-classes-0.4.14
- semirings-0.2.1.1 - semirings-0.2.1.1
- systemd-1.1.2
resolver: lts-10.5 resolver: lts-10.5

View File

@ -74,3 +74,9 @@
filter: grayscale(1); filter: grayscale(1);
} }
} }
/* special treatment for checkboxes in table headers */
th .checkbox {
margin-right: 7px;
vertical-align: bottom;
}

View File

@ -96,9 +96,9 @@
checkAllCheckbox.setAttribute('id', getCheckboxId()); checkAllCheckbox.setAttribute('id', getCheckboxId());
th.insertBefore(checkAllCheckbox, th.firstChild); th.insertBefore(checkAllCheckbox, th.firstChild);
// manually set up newly created checkbox // manually set up new checkbox
if (UtilRegistry) { if (UtilRegistry) {
UtilRegistry.setup(UtilRegistry.find('checkbox')); UtilRegistry.setup(UtilRegistry.find('checkbox'), th);
} }
checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput); checkAllCheckbox.addEventListener('input', onCheckAllCheckboxInput);

View File

@ -0,0 +1,22 @@
<section>
<p>
Das Hochladen einer Korrekturen markiert die entsprechende
Abgabe automatisch als "korrigiert", falls Ihnen die Abgabe zugeteilt gewesen war.
<p>
Lädt jedoch ein Assistent Korrekturen hoch, welche anderen Korrektoren
oder noch nicht zugeteilt wurden, so werden diese Abgaben noch nicht als "korrigiert" markiert.
<p>
Es ist geplant, dass die Bewertungsdatei in Zukunft ein eigenes Feld enthält,
in dem Korrektoren angeben können, ob die Korrektur abgeschlossen ist oder nicht.
<p>
Im Gegensatz zu UniWorX enthalten die heruntergeladenen Abgaben immer den
aktuellen Stand der Bewertung. Dies betrifft ggf. auch geänderte Dateien!
<section>
<p>
Bei der Korrektur können Dateien verändert, hinzugefügt und gelöscht werden.
Die Abgebenden werden entsprechend informiert, sobald die Abgabe als "korrigiert" markiert wurde.
<p>
Temporäre Dateien einer eventuellen Vorkorrektur müssen also durch das Hochladen der
Korrekturen des letzten Korrektors gelöscht werden, falls diese den Abgabenden
nicht zur Verfügung gestellt werden sollen.

View File

@ -1 +1,4 @@
^{uploadForm} <section>
^{uploadInstruction}
<section>
^{uploadForm}

View File

@ -2,10 +2,10 @@
$maybe flag <- sortableKey $maybe flag <- sortableKey
$case directions $case directions
$of [SortAsc] $of [SortAsc]
<a .table__th-link href=^{tblLink' $ setParam (wIdent "sorting") (Just $ toPathPiece (SortingSetting flag SortDesc))}> <a .table__th-link href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
^{widget} ^{widget}
$of _ $of _
<a .table__th-link href=^{tblLink' $ setParam (wIdent "sorting") (Just $ toPathPiece (SortingSetting flag SortAsc))}> <a .table__th-link href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
^{widget} ^{widget}
$nothing $nothing
^{widget} ^{widget}

View File

@ -3,14 +3,20 @@ $if not (null activeCategories)
<div .recipient-categories> <div .recipient-categories>
$forall category <- activeCategories $forall category <- activeCategories
<div .recipient-category> <div .recipient-category>
<input type=checkbox id=#{checkedIdent category} :elem category checkedCategories:checked> <input type=checkbox id=#{checkedIdent category} .recipient-category__checkbox :elem category checkedCategories:checked>
<label .recipient-category__label for=#{checkedIdent category}> <label .recipient-category__label for=#{checkedIdent category}>
_{category} _{category}
$if hasContent category $if hasContent category
<fieldset .recipient-category__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{checkedIdent category}> <fieldset .recipient-category__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{checkedIdent category}>
$forall optIx <- categoryIndices category $if not (null (categoryIndices category))
^{cellWdgts ! optIx} <div .recipient-category__checked-counter>
<div .recipient-category__toggle-all>
<input type=checkbox id=#{checkedIdent category}-toggle-all>
<label for=#{checkedIdent category}-toggle-all .recipient-category__option-label>_{MsgRecipientToggleAll}
<div .recipient-category__options>
$forall optIx <- categoryIndices category
^{cellWdgts ! optIx}
$maybe addWdgt <- addWdgts !? (1, (EnumPosition category, 0)) $maybe addWdgt <- addWdgts !? (1, (EnumPosition category, 0))
^{addWdgt} ^{addWdgt}

View File

@ -0,0 +1,87 @@
(function() {
var MASS_INPUT_SELECTOR = '.massinput';
var RECIPIENT_CATEGORIES_SELECTOR = '.recipient-categories';
var RECIPIENT_CATEGORY_SELECTOR = '.recipient-category';
var RECIPIENT_CATEGORY_CHECKBOX_SELECTOR = '.recipient-category__checkbox ';
var RECIPIENT_CATEGORY_OPTIONS_SELECTOR = '.recipient-category__options';
var RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR = '.recipient-category__toggle-all [type="checkbox"]';
var RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR = '.recipient-category__checked-counter';
var massInputElement;
document.addEventListener('DOMContentLoaded', function() {
var recipientCategoriesElement = document.querySelector(RECIPIENT_CATEGORIES_SELECTOR);
massInputElement = recipientCategoriesElement.closest(MASS_INPUT_SELECTOR);
setupRecipientCategories();
var recipientObserver = new MutationObserver(setupRecipientCategories);
recipientObserver.observe(massInputElement, { childList: true });
});
function setupRecipientCategories() {
var recipientCategoryElements = Array.from(massInputElement.querySelectorAll(RECIPIENT_CATEGORY_SELECTOR));
recipientCategoryElements.forEach(function(element) {
setupRecipientCategory(element);
});
}
function setupRecipientCategory(recipientCategoryElement) {
var categoryCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKBOX_SELECTOR);
var categoryOptions = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_OPTIONS_SELECTOR);
if (categoryOptions) {
var categoryCheckboxes = Array.from(categoryOptions.querySelectorAll('[type="checkbox"]'));
var toggleAllCheckbox = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_TOGGLE_ALL_SELECTOR);
// setup category checkbox to toggle all child checkboxes if changed
categoryCheckbox.addEventListener('change', function() {
categoryCheckboxes.forEach(function(checkbox) {
checkbox.checked = categoryCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
// update counter and toggle checkbox initially
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
// register change listener for individual checkboxes
categoryCheckboxes.forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes);
});
});
// register change listener for toggle all checkbox
if (toggleAllCheckbox) {
toggleAllCheckbox.addEventListener('change', function() {
categoryCheckboxes.forEach(function(checkbox) {
checkbox.checked = toggleAllCheckbox.checked;
});
updateCheckedCounter(recipientCategoryElement, categoryCheckboxes);
});
}
}
}
// update checked state of toggle all checkbox based on all other checkboxes
function updateToggleAllCheckbox(toggleAllCheckbox, categoryCheckboxes) {
var allChecked = categoryCheckboxes.reduce(function(acc, checkbox) {
return acc && checkbox.checked;
}, true);
toggleAllCheckbox.checked = allChecked;
}
// update value of checked counter
function updateCheckedCounter(recipientCategoryElement, categoryCheckboxes) {
var checkedCounter = recipientCategoryElement.querySelector(RECIPIENT_CATEGORY_CHECKED_COUNTER_SELECTOR);
var checkedCheckboxes = categoryCheckboxes.reduce(function(acc, checkbox) { return checkbox.checked ? acc + 1 : acc; }, 0);
checkedCounter.innerHTML = checkedCheckboxes + '/' + categoryCheckboxes.length;
}
})();

View File

@ -11,6 +11,11 @@
} }
} }
.recipient-category__options {
max-height: 150px;
overflow-y: auto;
}
.recipient-category__option { .recipient-category__option {
display: flex; display: flex;
@ -30,8 +35,7 @@
padding: 5px 0 10px; padding: 5px 0 10px;
border-left: 1px solid #bcbcbc; border-left: 1px solid #bcbcbc;
padding-left: 16px; padding-left: 16px;
max-height: 200px; position: relative;
overflow-y: auto;
} }
.recipient-category__option-add { .recipient-category__option-add {
@ -42,3 +46,20 @@
padding: 10px 0; padding: 10px 0;
} }
} }
.recipient-category__options + .recipient-category__option-add {
margin-top: 10px;
}
.recipient-category__toggle-all {
display: flex;
border-bottom: 1px solid #bcbcbc;
padding-bottom: 8px;
margin-bottom: 8px;
}
.recipient-category__checked-counter {
position: absolute;
right: 5px;
top: 5px;
}