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

View File

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

View File

@ -24,7 +24,8 @@ import Language.Haskell.TH.Syntax (qLocation)
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Settings, defaultSettings,
defaultShouldDisplayException,
runSettings, setHost,
runSettings, runSettingsSocket, setHost,
setBeforeMainLoop,
setOnException, setPort, getPort)
import Network.Wai.Middleware.RequestLogger (Destination (Logger),
IPAddrSource (..),
@ -71,6 +72,8 @@ import qualified Data.Aeson as Aeson
import System.Exit (exitFailure)
import qualified Database.Memcached.Binary.IO as Memcached
import qualified System.Systemd.Daemon as Systemd
-- Import all relevant handler modules here.
-- (HPack takes care to add new modules to our cabal file nowadays.)
@ -291,6 +294,7 @@ warpSettings :: UniWorX -> Settings
warpSettings foundation = defaultSettings
& setPort (foundation ^. _appPort)
& setHost (foundation ^. _appHost)
& setBeforeMainLoop (void Systemd.notifyReady)
& setOnException (\_req e ->
when (defaultShouldDisplayException e) $ do
logger <- readTVarIO . snd $ appLogger foundation
@ -338,7 +342,12 @@ appMain = runResourceT $ do
app <- makeApplication foundation
-- 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
}
defaultLayout
defaultLayout $ do
let uploadInstruction = $(i18nWidgetFile "corrections-upload-instructions")
$(widgetFile "corrections-upload")
getCorrectionsCreateR, postCorrectionsCreateR :: Handler Html

View File

@ -553,7 +553,7 @@ courseEditHandler miButtonAction mbCourseForm = do
case insertRes of
Just _ ->
queueDBJob . JobLecturerInvitation aid $ LecturerInvitation lEmail cid mLTy
Nothing ->
Nothing ->
updateBy (UniqueLecturerInvitation lEmail cid) [ LecturerInvitationType =. mLTy ]
insert_ $ CourseEdit aid now cid
addMessageI Success $ MsgCourseEditOk tid ssh csh
@ -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
-- Note that order of E.on for nested joins is seemingly right-to-left, ignoring nesting paranthesis
features <- studyFeaturesQuery' (participant E.^. CourseParticipantField) studyFeatures
E.on $ E.just (participant E.^. CourseParticipantUser) E.==. note E.?. CourseUserNoteUser
E.on $ participant E.^. CourseParticipantUser E.==. user E.^. UserId
E.on $ (note E.?. CourseUserNoteUser E.==. E.just (participant E.^. CourseParticipantUser))
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
return (user, participant E.^. CourseParticipantRegistration, note E.?. CourseUserNoteId, features)
@ -1130,7 +1131,7 @@ postCCommR tid ssh csh = do
evalAccessDB (CourseR tid ssh csh $ CUserR cID) False
}
data ButtonLecInvite = BtnLecInvAccept | BtnLecInvDecline
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
instance Universe ButtonLecInvite

View File

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

View File

@ -3,7 +3,7 @@ module Handler.Sheet where
import Import
import Jobs.Queue
import System.FilePath (takeFileName)
import Utils.Sheet
@ -642,7 +642,7 @@ correctorForm shid = wFormToAForm $ do
Just currentRoute <- liftHandlerT getCurrentRoute
userId <- liftHandlerT requireAuthId
MsgRenderer mr <- getMsgRenderer
let
currentLoads :: DB Loads
currentLoads = Map.union
@ -661,7 +661,7 @@ correctorForm shid = wFormToAForm $ do
when (not (Map.null loads) && applyDefaultLoads) $
addMessageI Warning MsgCorrectorsDefaulted
countTutRes <- wreq checkBoxField (fsm MsgCountTutProp) . Just . any (\(_, Load{..}) -> fromMaybe False byTutorial) $ Map.elems loads
let
@ -673,7 +673,7 @@ correctorForm shid = wFormToAForm $ do
E.on $ sheetCorrector E.^. SheetCorrectorUser E.==. user E.^. UserId
E.where_ $ lecturer E.^. LecturerUser E.==. E.val userId
return user
miAdd :: ListPosition
-> Natural
-> (Text -> Text)
@ -710,7 +710,7 @@ correctorForm shid = wFormToAForm $ do
User{userEmail, userDisplayName, userSurname} <- liftHandlerT . runDB $ getJust uid
return $ nameEmailWidget userEmail userDisplayName userSurname
return (res, $(widgetFile "sheetCorrectors/cell"))
miDelete :: ListLength
-> ListPosition
@ -748,7 +748,7 @@ correctorForm shid = wFormToAForm $ do
where
sheetCorrectorSheet = shid
sheetCorrectorInvitationSheet = shid
postProcess' :: (Either UserEmail UserId, (CorrectorState, Load)) -> Either SheetCorrectorInvitation SheetCorrector
postProcess' (Right sheetCorrectorUser, (sheetCorrectorState, sheetCorrectorLoad)) = Right SheetCorrector{..}
postProcess' (Left sheetCorrectorInvitationEmail, (sheetCorrectorInvitationState, sheetCorrectorInvitationLoad)) = Left SheetCorrectorInvitation{..}

View File

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

View File

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

View File

@ -1,7 +1,7 @@
module Utils.PathPiece
( finiteFromPathPiece
, nullaryToPathPiece
, nullaryPathPiece
, nullaryPathPiece, finitePathPiece
, splitCamel
, camelToPathPiece, camelToPathPiece'
, tuplePathPiece
@ -16,6 +16,9 @@ import Data.Universe
import qualified Data.Text as Text
import qualified Data.Char as Char
import Data.Map ((!), (!?))
import qualified Data.Map as Map
import Numeric.Natural
import Data.List (foldl)
@ -44,6 +47,16 @@ nullaryPathPiece nullaryType mangle =
, funD 'fromPathPiece
[ 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 = map fromList . reverse . helper (error "hasChange undefined at start of string") [] "" . otoList

View File

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

View File

@ -74,3 +74,9 @@
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());
th.insertBefore(checkAllCheckbox, th.firstChild);
// manually set up newly created checkbox
// manually set up new checkbox
if (UtilRegistry) {
UtilRegistry.setup(UtilRegistry.find('checkbox'));
UtilRegistry.setup(UtilRegistry.find('checkbox'), th);
}
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
$case directions
$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}
$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}
$nothing
^{widget}

View File

@ -3,14 +3,20 @@ $if not (null activeCategories)
<div .recipient-categories>
$forall category <- activeCategories
<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}>
_{category}
$if hasContent category
<fieldset .recipient-category__fieldset uw-interactive-fieldset .interactive-fieldset__target data-conditional-input=#{checkedIdent category}>
$forall optIx <- categoryIndices category
^{cellWdgts ! optIx}
$if not (null (categoryIndices category))
<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))
^{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 {
display: flex;
@ -30,8 +35,7 @@
padding: 5px 0 10px;
border-left: 1px solid #bcbcbc;
padding-left: 16px;
max-height: 200px;
overflow-y: auto;
position: relative;
}
.recipient-category__option-add {
@ -42,3 +46,20 @@
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;
}