feat(course-participants): show exercise sheets (first cornice)

This commit is contained in:
Gregor Kleen 2020-06-14 16:26:48 +02:00
parent e10cfe9c58
commit 26cc8e4b53
13 changed files with 373 additions and 159 deletions

View File

@ -425,6 +425,7 @@ input[type="button"].btn-info:not(.btn-link):hover,
padding-bottom: 10px
font-weight: bold
text-align: left
vertical-align: middle
a
color: white

View File

@ -115,6 +115,7 @@ export class UtilRegistry {
} catch(err) {
if (DEBUG_MODE > 0) {
console.error('Error while trying to initialize a utility!', { util , element, err });
console.error(err.stack);
}
utilInstance = null;
}

View File

@ -8,13 +8,10 @@ const CHECK_ALL_INITIALIZED_CLASS = 'check-all--initialized';
selector: 'table:not([uw-no-check-all])',
})
export class CheckAll {
_element;
_app;
_columns = [];
_checkboxColumn = [];
_checkAllCheckbox = null;
_checkAllColumns = [];
constructor(element, app) {
if (!element) {
@ -22,80 +19,81 @@ export class CheckAll {
}
this._element = element;
this._app = app;
if (this._element.classList.contains(CHECK_ALL_INITIALIZED_CLASS)) {
return false;
}
this._gatherColumns();
this._setupCheckAllCheckbox();
this._findCheckboxColumns().forEach(columnId => this._checkAllColumns.push(new CheckAllColumn(this._element, app, this._columns[columnId])));
// mark initialized
this._element.classList.add(CHECK_ALL_INITIALIZED_CLASS);
}
destroy() {
this._checkAllCheckbox.destroy();
}
_getCheckboxId() {
return 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
}
_gatherColumns() {
const rows = Array.from(this._element.querySelectorAll('tr'));
const rows = Array.from(this._element.rows);
const cols = [];
rows.forEach((tr) => {
const cells = Array.from(tr.querySelectorAll('td'));
cells.forEach((cell, cellIndex) => {
if (!cols[cellIndex]) {
cols[cellIndex] = [];
const cells = Array.from(tr.cells);
cells.forEach(cell => {
let i = 0;
for (const sibling of cells.slice(0, cell.cellIndex))
i += Math.max(1, sibling.colSpan) || 1;
for (let j = i; j < i + cell.colSpan; j++) {
if (!cols[j]) {
cols[j] = [];
}
cols[j].push(cell);
}
cols[cellIndex].push(cell);
});
});
this._columns = cols;
}
_findCheckboxColumn(columns) {
let checkboxColumnId = null;
columns.forEach((col, i) => {
_findCheckboxColumns() {
let checkboxColumnIds = [];
this._columns.forEach((col, i) => {
if (this._isCheckboxColumn(col)) {
checkboxColumnId = i;
checkboxColumnIds.push(i);
}
});
return checkboxColumnId;
return checkboxColumnIds;
}
_isCheckboxColumn(col) {
let onlyCheckboxes = true;
col.forEach((cell) => {
if (onlyCheckboxes && !cell.querySelector(CHECKBOX_SELECTOR)) {
onlyCheckboxes = false;
}
});
return onlyCheckboxes;
return col.every(cell => cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR))
&& col.some(cell => cell.querySelector(CHECKBOX_SELECTOR));
}
}
_setupCheckAllCheckbox() {
const checkboxColumnId = this._findCheckboxColumn(this._columns);
if (checkboxColumnId === null) {
return;
}
class CheckAllColumn {
_app;
_table;
_column;
_checkAllCheckbox;
_checkboxId = 'check-all-checkbox-' + Math.floor(Math.random() * 100000);
constructor(table, app, column) {
this._column = column;
this._table = table;
this._app = app;
const th = this._column.filter(element => element.tagName == 'TH')[0];
if (!th)
return false;
this._checkboxColumn = this._columns[checkboxColumnId];
const firstRow = this._element.querySelector('tr');
const th = Array.from(firstRow.querySelectorAll('th, td'))[checkboxColumnId];
this._checkAllCheckbox = document.createElement('input');
this._checkAllCheckbox.setAttribute('type', 'checkbox');
this._checkAllCheckbox.setAttribute('id', this._getCheckboxId());
this._checkAllCheckbox.setAttribute('id', this._checkboxId);
th.insertBefore(this._checkAllCheckbox, th.firstChild);
// set up new checkbox
this._app.utilRegistry.initAll(th);
this._checkAllCheckbox.addEventListener('input', () => this._onCheckAllCheckboxInput());
this._checkAllCheckbox.addEventListener('input', this._onCheckAllCheckboxInput.bind(this));
this._setupCheckboxListeners();
}
@ -104,23 +102,25 @@ export class CheckAll {
}
_setupCheckboxListeners() {
this._checkboxColumn.map((cell) => {
return cell.querySelector(CHECKBOX_SELECTOR);
})
.forEach((checkbox) => {
checkbox.addEventListener('input', () => this._updateCheckAllCheckboxState());
});
this._column
.flatMap(cell => cell.tagName == 'TH' ? new Array() : Array.from(cell.querySelectorAll(CHECKBOX_SELECTOR)))
.forEach(checkbox =>
checkbox.addEventListener('input', this._updateCheckAllCheckboxState.bind(this))
);
}
_updateCheckAllCheckboxState() {
const allChecked = this._checkboxColumn.every((cell) => {
return cell.querySelector(CHECKBOX_SELECTOR).checked;
});
const allChecked = this._column.every(cell =>
cell.tagName == 'TH' || cell.querySelector(CHECKBOX_SELECTOR).checked
);
this._checkAllCheckbox.checked = allChecked;
}
_toggleAll(checked) {
this._checkboxColumn.forEach((cell) => {
this._column.forEach(cell => {
if (cell.tagName == 'TH')
return;
cell.querySelector(CHECKBOX_SELECTOR).checked = checked;
});
}

View File

@ -1,11 +1,13 @@
import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import './hide-columns.sass';
import * as memoize from 'lodash.memoize';
const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header';
const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label';
const HIDE_COLUMNS_NO_HIDE = 'uw-hide-columns--no-hide';
const HIDE_COLUMNS_DEFAULT_HIDDEN = 'uw-hide-column-default-hidden';
const TABLE_UTILS_ATTR = 'table-utils';
const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`;
@ -18,6 +20,8 @@ const TABLE_PILL_CLASS = 'table-pill';
const CELL_HIDDEN_CLASS = 'hide-columns--hidden-cell';
const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan';
const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized';
@Utility({
selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`,
})
@ -44,14 +48,15 @@ export class HideColumns {
constructor(element) {
this._autoHide = this._storageManager.load('autoHide', {}) || false;
if (!element) {
if (!element)
throw new Error('Hide Columns utility cannot be setup without an element!');
}
// do not provide hide-column ability in tables inside modals, async forms with response or tail.datetime instances
if (element.closest('[uw-modal], .async-form__response, .tail-datetime-calendar')) {
if (element.closest('[uw-modal], .async-form__response, .tail-datetime-calendar'))
return false;
if (element.classList.contains(HIDE_COLUMNS_INITIALIZED))
return false;
}
this._element = element;
@ -74,10 +79,12 @@ export class HideColumns {
this._mutationObserver = new MutationObserver(this._tableMutated.bind(this));
this._mutationObserver.observe(this._element, { childList: true, subtree: true });
this._element.classList.add(HIDE_COLUMNS_INITIALIZED);
}
setupHideButton(th) {
const preHidden = this.isHiddenColumn(th);
const preHidden = this.isHiddenTH(th);
const hider = document.createElement('span');
@ -104,20 +111,20 @@ export class HideColumns {
hider.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
this.switchColumnDisplay(th, hider);
this.switchColumnDisplay(th);
// this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider));
});
hider.addEventListener('mouseover', () => {
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
const currentlyHidden = this.isHiddenColumn(th);
const currentlyHidden = this.hiderStatus(th);
this.updateHiderIcon(hider, !currentlyHidden);
});
hider.addEventListener('mouseout', () => {
if (hider.classList.contains(TABLE_HIDER_CLASS)) {
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
}
const currentlyHidden = this.isHiddenColumn(th);
const currentlyHidden = this.hiderStatus(th);
this.updateHiderIcon(hider, currentlyHidden);
});
@ -126,64 +133,76 @@ export class HideColumns {
// reposition hider on each window resize event
// window.addEventListener('resize', () => this.repositionHider(hider));
this.updateColumnDisplay(this.colIndex(th), preHidden);
this.updateHider(hider, preHidden);
if (preHidden) {
this._tableUtilContainer.appendChild(hider);
} else {
this.hideHiderBehindHeader(hider);
}
this.switchColumnDisplay(th, preHidden);
}
switchColumnDisplay(th, hider) {
const hidden = !this.isHiddenColumn(th);
const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1;
const colspan = Math.max(1, th.colSpan) || 1;
const columnIndex = this.colIndex(th);
switchColumnDisplay(th, hidden) {
hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden;
for (let i = 0; i < Math.max(colspan, originalColspan); i++) {
this.updateColumnDisplay(columnIndex + i, hidden);
}
this.updateHider(hider, hidden);
// persist new hidden setting for column
if ((hidden && this.isEmptyColumn(columnIndex) && this._autoHide) || (!hidden && (!this.isEmptyColumn(columnIndex) || !this._autoHide))) {
this._storageManager.remove(this.getStorageKey(th));
} else {
this._storageManager.save(this.getStorageKey(th), hidden);
}
this.cellColumns(th).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
}
updateColumnDisplay(columnIndex, hidden) {
this._element.getElementsByTagName('tr').forEach(row => {
// console.debug('updateColumnDisplay', { columnIndex, hidden });
this._element.rows.forEach(row => {
const cell = this.getCol(row, columnIndex);
if (cell) {
const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
const colspan = Math.max(1, cell.colSpan) || 1;
const visibleColumns = this.cellColumns(cell).reduce((count, cColumnIndex) => (cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex)) ? count : count + 1, 0);
if (hidden) {
if (colspan > 1) {
// if (cell.tagName === 'TH') {
// console.debug({cell, originalColspan, colspan, visibleColumns, isHidden: cell.classList.contains(CELL_HIDDEN_CLASS)});
// }
if (visibleColumns <= 0) {
cell.classList.add(CELL_HIDDEN_CLASS);
} else {
cell.classList.remove(CELL_HIDDEN_CLASS);
if (colspan !== visibleColumns) {
if (!originalColspan) {
cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan);
}
cell.colSpan--;
} else {
cell.classList.add(CELL_HIDDEN_CLASS);
}
} else {
if (cell.classList.contains(CELL_HIDDEN_CLASS)) {
cell.classList.remove(CELL_HIDDEN_CLASS);
} else if (originalColspan && colspan < originalColspan) {
cell.colSpan++;
cell.colSpan = visibleColumns;
}
}
}
});
const touchedColumns = new Array();
this.columnTHs(columnIndex)
.forEach(th => {
touchedColumns.push(...this.cellColumns(th));
if (!this._element.classList.contains(HIDE_COLUMNS_INITIALIZED))
return;
const thHidden = this.cellColumns(th).every(cColumnIndex => cColumnIndex === columnIndex ? hidden : this.isHiddenColumn(cColumnIndex));
// persist new hidden setting for column
if (thHidden == this.isDefaultHiddenTH(th)) {
this._storageManager.remove(this.getStorageKey(th));
} else {
this._storageManager.save(this.getStorageKey(th), thHidden);
}
});
touchedColumns.flatMap(cColumnIndex => this.columnTHs(cColumnIndex))
.forEach(th => {
const hider = this.headerToHider.get(th);
if (!hider)
return;
this.updateHider(hider, this.hiderStatus(th));
});
}
updateHider(hider, hidden) {
// console.debug({hider, hidden, columnIndex: this.colIndex(this.hiderToHeader.get(hider)), colSpan: this.colSpan(this.hiderToHeader.get(hider))});
if (hidden) {
hider.classList.remove(TABLE_HIDER_CLASS);
hider.classList.add(TABLE_PILL_CLASS);
@ -223,12 +242,21 @@ export class HideColumns {
}
_tableMutated(mutationList) {
// console.log('_tableMutated', mutationList, observer);
if (!Array.from(mutationList).some(mutation => mutation.type === 'childList'))
return;
[...this._element.querySelectorAll('th')].filter(th => !th.hasAttribute(HIDE_COLUMNS_NO_HIDE)).forEach(th => this.updateColumnDisplay(this.colIndex(th), this.isHiddenColumn(th)));
if (Array.from(mutationList).every(mRecord => mRecord.type === 'childList' && [...mRecord.addedNodes, ...mRecord.removedNodes].every(isTableHider)))
return;
// console.debug('_tableMutated', { mutationList });
this._colIndexMemoized = undefined;
this._getColMemoized = undefined;
Array.from(this._element.rows)
.flatMap(row => Array.from(row.cells))
.filter(th => th.tagName === 'TH' && !th.hasAttribute(HIDE_COLUMNS_NO_HIDE))
.forEach(th => this.updateColumnDisplay(this.colIndex(th), this.isHiddenTH(th)));
}
getStorageKey(th) {
@ -257,9 +285,9 @@ export class HideColumns {
}
isEmptyColumn(columnIndex) {
for (let row of this._element.getElementsByTagName('tr')) {
for (let row of this._element.rows) {
const cell = this.getCol(row, columnIndex);
if (cell.matches('th'))
if (!cell || cell.tagName == 'TH')
continue;
if (cell.querySelector('.table__td-content')) {
for (let child of cell.children) {
@ -273,10 +301,47 @@ export class HideColumns {
}
}
isHiddenColumn(th) {
const hidden = this._storageManager.load(this.getStorageKey(th)),
emptyColumn = this.isEmptyColumn(this.colIndex(th));
return hidden === true || hidden === undefined && emptyColumn && this._autoHide;
columnTHs(columnIndex) {
return Array.from(this._element.rows)
.map(row => this.getCol(row, columnIndex))
.filter(cell => cell && cell.tagName === 'TH');
}
cellColumns(cell) {
const columnIndex = this.colIndex(cell);
return Array.from(new Array(this.colSpan(cell)), (_x, i) => columnIndex + i);
}
isHiddenTH(th) {
return this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex));
}
hiderStatus(th) {
const columnsHidden = this.isHiddenTH(th);
const shadowed = this.cellColumns(th).every(columnIndex => this.isHiddenColumn(columnIndex, this.columnTHs(columnIndex).filter(oTH => oTH !== th)));
const isFirst = this.cellColumns(th).some(columnIndex => this.columnTHs(columnIndex)[0] === th);
// console.debug("hiderStatus", { th, columnsHidden, shadowed, isFirst });
return columnsHidden && (!shadowed || isFirst);
}
isDefaultHiddenTH(th) {
return this.cellColumns(th).every(columnIndex => this.isDefaultHiddenColumn(columnIndex, Array.of(th)));
}
isHiddenColumn(columnIndex, ths) {
ths = ths === undefined ? this.columnTHs(columnIndex) : ths;
const hidden = ths.map(th => this._storageManager.load(this.getStorageKey(th)));
return hidden.every(h => h === undefined) ? this.isDefaultHiddenColumn(columnIndex, ths) : hidden.some(h => h);
}
isDefaultHiddenColumn(columnIndex, ths) {
ths = ths === undefined ? this.columnTHs(columnIndex) : ths;
return this.isEmptyColumn(columnIndex) && this._autoHide || ths.some(th => th.hasAttribute(HIDE_COLUMNS_DEFAULT_HIDDEN));
}
colSpan(cell) {
@ -289,7 +354,7 @@ export class HideColumns {
return originalColspan ? Math.max(colspan, originalColspan) : colspan;
}
colIndex(cell) {
colIndexDirect(cell) {
if (!cell)
return 0;
@ -298,23 +363,62 @@ export class HideColumns {
if (!rowParent)
return 0;
let i = 0;
for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
i += this.colSpan(sibling);
let rowBefore = 0;
for (const cell of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
if (!cell)
continue;
rowBefore += this.colSpan(cell);
}
let i = 0;
for (const pRow of Array.from(this._element.rows).slice(0, rowParent.rowIndex + 1)) {
if (!pRow)
continue;
let space = 0;
for (const cell of pRow === rowParent ? Array.from(pRow.cells).slice(0, cell.cellIndex) : pRow.cells) {
if (!cell)
continue;
const rowSpan = cell.rowSpan || 1;
if (rowParent.rowIndex - pRow.rowIndex < rowSpan)
i += this.colSpan(cell);
else if (pRow !== rowParent)
space += this.colSpan(cell);
if (space > rowBefore)
break;
}
}
// console.debug({ rowParent, cell, rowBefore, i });
return i;
}
getCol(row, columnIndex) {
let c = 0;
_colIndexMemoized;
for (const cell of row.cells) {
c += cell ? this.colSpan(cell) : 1;
colIndex(cell) {
if (!this._colIndexMemoized)
this._colIndexMemoized = memoize(this.colIndexDirect.bind(this));
if (columnIndex < c)
return this._colIndexMemoized(cell);
}
getColDirect(row, columnIndex) {
for (const cell of row.cells)
if (cell && this.colIndex(cell) <= columnIndex && this.colIndex(cell) + this.colSpan(cell) > columnIndex)
return cell;
}
}
_getColMemoized;
getCol(row, columnIndex) {
if (!this._getColMemoized)
this._getColMemoized = memoize(this.getColDirect.bind(this), (row, columnIndex) => Array.of(row.rowIndex, columnIndex).toString());
return this._getColMemoized(row, columnIndex);
}
}
@ -326,3 +430,8 @@ function isEmptyElement(element) {
return true;
}
function isTableHider(element) {
return element.classList.contains(TABLE_HIDER_CLASS)
|| element.classList.contains(TABLE_HIDER_VISIBLE_CLASS)
|| element.classList.contains(TABLE_PILL_CLASS);
}

View File

@ -2599,6 +2599,7 @@ CourseParticipantActive: Teilnehmer
CourseParticipantInactive: Abgemeldet
CourseParticipantNoShow: Nicht erschienen
CourseUserState: Zustand
CourseUserSheets: Übungsblätter
TestDownload: Download-Test
TestDownloadMaxSize: Maximale Dateigröße

View File

@ -2599,6 +2599,7 @@ CourseParticipantActive: Participant
CourseParticipantInactive: Deregistered
CourseParticipantNoShow: No show
CourseUserState: State
CourseUserSheets: Exercise sheets
TestDownload: Download test
TestDownloadMaxSize: Maximum filesize

View File

@ -1,4 +1,4 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-redundant-constraints #-}
module Handler.Course.Users
( queryUser
@ -98,6 +98,7 @@ type UserTableData = DBRow ( Entity User
, ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial)))
, [Entity Exam]
, Maybe (Entity SubmissionGroup)
, Map SheetName (SheetType, Maybe Points)
)
instance HasEntity UserTableData User where
@ -130,13 +131,15 @@ _userExams = _dbrOutput . _6
_userSubmissionGroup :: Traversal' UserTableData (Entity SubmissionGroup)
_userSubmissionGroup = _dbrOutput . _7 . _Just
_userSheets :: Lens' UserTableData (Map SheetName (SheetType, Maybe Points))
_userSheets = _dbrOutput . _8
colUserComment :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c)
colUserComment tid ssh csh =
sortable (Just "note") (i18nCell MsgCourseUserNote)
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _, _) } ->
maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (hasComment True)
sortable (Just "note") (i18nCell MsgCourseUserNote) $ views (_dbrOutput . $(multifocusG 2) (_1 . _entityKey) _3) $ \(uid, mbNoteKey) ->
maybeEmpty mbNoteKey $ const $
anchorCellM (courseLink <$> encrypt uid) (hasComment True)
where
courseLink = CourseR tid ssh csh . CUserR
@ -183,6 +186,20 @@ colUserSubmissionGroup :: IsDBTable m c => Colonnade Sortable UserTableData (DBC
colUserSubmissionGroup = sortable (Just "submission-group") (i18nCell MsgSubmissionGroup) $
foldMap (cell . toWidget) . preview (_userSubmissionGroup . _entityVal . _submissionGroupName)
colUserSheets :: forall m c. IsDBTable m c => [SheetName] -> Cornice Sortable ('Cap 'Base) UserTableData (DBCell m c)
colUserSheets shns = cap (Sortable Nothing caption) $ foldMap userSheetCol shns
where
caption = i18nCell MsgCourseUserSheets
& cellAttrs <>~ [ ("uw-hide-column-header", "sheets")
, ("uw-hide-column-default-hidden", "")
]
userSheetCol :: SheetName -> Colonnade Sortable UserTableData (DBCell m c)
userSheetCol shn = sortable (Just . SortingKey $ "sheet-" <> shn) (i18nCell shn) . views (_userSheets . at shn) $ \case
Just (preview _grading -> Just Points{..}, Just points) -> i18nCell $ MsgAchievedOf points maxPoints
Just (preview _grading -> Just grading', Just points) -> i18nCell . bool MsgNotPassed MsgPassed . fromMaybe False $ gradingPassed grading' points
_other -> mempty
data UserTableCsvStudyFeature = UserTableCsvStudyFeature
{ csvUserField :: Text
@ -310,14 +327,15 @@ data CourseUserActionData = CourseUserSendMailData
deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeCourseUserTable :: forall h act act'.
makeCourseUserTable :: forall h p cols act act'.
( Functor h, ToSortable h
, Ord act, PathPiece act, RenderMessage UniWorX act
, AsCornice h p UserTableData (DBCell (MForm Handler) (FormResult (First act', DBFormResult UserId Bool UserTableData))) cols
)
=> CourseId
-> Map act (AForm Handler act')
-> (UserTableExpr -> E.SqlExpr (E.Value Bool))
-> Colonnade h UserTableData (DBCell (MForm Handler) (FormResult (First act', DBFormResult UserId Bool UserTableData)))
-> cols
-> PSValidator (MForm Handler) (FormResult (First act', DBFormResult UserId Bool UserTableData))
-> Maybe (Csv.Name -> Bool)
-> DB (FormResult (act', Set UserId), Widget)
@ -336,12 +354,23 @@ makeCourseUserTable cid acts restrict colChoices psValidator csvColumns = do
dbtProj = traverse $ \(user, participant, E.Value userNoteId, (feature,degree,terms), subGroup) -> do
tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] []
exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] []
subs' <- E.select . E.from $ \(sheet `E.InnerJoin` (submission `E.InnerJoin` submissionUser)) -> do
E.on $ submissionUser E.^. SubmissionUserSubmission E.==. submission E.^. SubmissionId
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.&&. submissionUser E.^. SubmissionUserUser E.==. E.val (entityKey user)
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
return ( sheet E.^. SheetName
, ( sheet E.^. SheetType
, submission
)
)
let
regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials
tuts' = filter (\(Entity tutId _) -> any ((== tutId) . tutorialParticipantTutorial . entityVal) tuts'') tutorials
tuts = foldr (\tut@(Entity _ Tutorial{..}) -> maybe (over _1 $ cons tut) (over _2 . flip (Map.insertWith (<|>)) (Just tut)) tutorialRegGroup) ([], Map.fromSet (const Nothing) regGroups) tuts'
exs = filter (\(Entity eId _) -> any ((== eId) . examRegistrationExam . entityVal) exams') exams
return (user, participant, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms), tuts, exs, subGroup)
subs = Map.fromList $ mapMaybe (over (mapped . _2 . _2) (submissionRatingPoints . entityVal) . assertM' (views (_2 . _2 . _entityVal) submissionRatingDone) . over _1 E.unValue . over (_2 . _1) E.unValue) subs'
return (user, participant, userNoteId, (entityVal <$> feature, entityVal <$> degree, entityVal <$> terms), tuts, exs, subGroup, subs)
dbtColonnade = colChoices
dbtSorting = mconcat
[ single $ sortUserNameLink queryUser -- slower sorting through clicking name column header
@ -541,22 +570,24 @@ postCUsersR tid ssh csh = do
E.&&. courseParticipant E.^. CourseParticipantCourse E.==. submissionGroup E.^. SubmissionGroupCourse
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid
sheetList <- selectList [SheetCourse ==. cid] [Desc SheetActiveTo, Desc SheetActiveFrom]
let exams = nubOn entityKey $ examOccurrencesPerExam ^.. folded . _1
let colChoices = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
, pure $ colUserNameLink (CourseR tid ssh csh . CUserR)
, guardOn showSex $ colUserSex'
, pure $ colUserEmail
, pure $ colUserMatriclenr
, pure $ colUserDegreeShort
, pure $ colUserField
, pure $ colUserSemester
, guardOn hasSubmissionGroups colUserSubmissionGroup
, guardOn hasTutorials $ colUserTutorials tid ssh csh
, guardOn hasExams $ colUserExams tid ssh csh
, pure $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (maybe mempty dateCell . preview (_Just . _userTableRegistration) . assertM' (has $ _userTableParticipant . _entityVal . _courseParticipantState . _CourseParticipantActive))
, pure $ sortable (Just "state") (i18nCell MsgCourseUserState) (i18nCell . view (_userTableParticipant . _entityVal . _courseParticipantState))
, pure $ colUserComment tid ssh csh
[ pure . cap' $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
, pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR)
, guardOn showSex . cap' $ colUserSex'
, pure . cap' $ colUserEmail
, pure . cap' $ colUserMatriclenr
, pure . cap' $ colUserDegreeShort
, pure . cap' $ colUserField
, pure . cap' $ colUserSemester
, guardOn hasSubmissionGroups $ cap' colUserSubmissionGroup
, guardOn hasTutorials . cap' $ colUserTutorials tid ssh csh
, guardOn hasExams . cap' $ colUserExams tid ssh csh
, pure . cap' $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (maybe mempty dateCell . preview (_Just . _userTableRegistration) . assertM' (has $ _userTableParticipant . _entityVal . _courseParticipantState . _CourseParticipantActive))
, pure . cap' $ sortable (Just "state") (i18nCell MsgCourseUserState) (i18nCell . view (_userTableParticipant . _entityVal . _courseParticipantState))
, guardOn (not $ null sheetList) . colUserSheets $ map (sheetName . entityVal) sheetList
, pure . cap' $ colUserComment tid ssh csh
]
psValidator = def & defaultSortingByName
& defaultFilter (singletonMap "active" [toPathPiece True])

View File

@ -42,6 +42,7 @@ module Handler.Utils.Table.Pagination
, formCell, DBFormResult(..), getDBFormResult
, dbSelect
, (&)
, cap'
, module Control.Monad.Trans.Maybe
, module Colonnade
, DBSTemplateMode(..)
@ -59,6 +60,8 @@ import Utils.Lens
import Import hiding (pi)
import qualified Data.Foldable as Foldable
import qualified Yesod.Form.Functions as Yesod
import qualified Database.Esqueleto as E
@ -68,12 +71,11 @@ import qualified Database.Esqueleto.Internal.Sql as E (SqlSelect,unsafeSqlValue)
import qualified Network.Wai as Wai
import Control.Monad.RWS (RWST(..), execRWS)
import Control.Monad.State (evalStateT)
import Control.Monad.State (evalStateT, execStateT)
import Control.Monad.Trans.Maybe
import Control.Monad.State.Class (modify)
import qualified Control.Monad.State.Class as State
import Data.Foldable (Foldable(foldMap))
import Control.Monad.Trans.Writer.Lazy (censor)
import Data.Map ((!))
import qualified Data.Map as Map
@ -90,7 +92,7 @@ import Colonnade.Encode hiding (row)
import Text.Hamlet (hamletFile)
import Data.List (elemIndex)
import Data.List (elemIndex, inits)
import Data.Maybe (fromJust)
@ -108,7 +110,8 @@ import qualified Data.ByteString.Lazy as LBS
import Data.Semigroup as Sem (Semigroup(..))
import qualified Data.Conduit.List as C
import qualified Data.Conduit.List as C (sourceList)
import qualified Data.Conduit.Combinators as C
import Handler.Utils.DateTime (formatTimeRangeW)
import qualified Control.Monad.Catch as Catch
@ -1136,7 +1139,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
| otherwise
= id
in C.sourceList <=< lift . doHandle . runConduit $ dbtCsvComputeActions x .| C.foldMap pure
innerAct .| C.fold accActionMap Map.empty
innerAct .| C.foldl accActionMap Map.empty
actionMap <- flip evalStateT Map.empty . runConduit $ sourceDiff .| transPipe lift dbtCsvComputeActions'
when (Map.null actionMap) $ do
@ -1266,12 +1269,47 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
DBSTDefault{..} -> dbstmNumber rowCount
_other -> False
genHeaders :: _ -> [WriterT x m Widget]
genHeaders SortableP{..} = flip (headersMonoidal Nothing) (annotate $ dbtColonnade ^. _Cornice) $ pure
( \Sized{ sizedSize, sizedContent = toSortable -> Sortable{..} } -> pure $ do
genHeaders :: forall h. Cornice h _ _ (DBCell m x) -> SortableP h -> WriterT x m Widget
genHeaders cornice SortableP{..} = execWriterT . go mempty $ annotate cornice
where
go :: forall (p' :: Pillar) r'.
[(Int, Int, Int)]
-> AnnotatedCornice (Maybe Int) h p' r' (DBCell m x)
-> WriterT Widget (WriterT x m) ()
go rowspanAcc (AnnotatedCorniceBase _ (Colonnade (toList -> v))) = censor wrap . forM_ (zip (inits v) v) $ \(before, OneColonnade Sized{..} _) -> do
let (_, cellSize') = compCellSize rowspanAcc (map oneColonnadeHead before) Sized{..}
whenIsJust cellSize' $ \cellSize -> tellM $ fromContent Sized { sizedSize = cellSize, sizedContent }
go rowspanAcc (AnnotatedCorniceCap _ v@(toList -> oneCornices)) = do
rowspanAcc' <- (execStateT ?? rowspanAcc) . hoist (censor wrap) . forM_ (zip (inits oneCornices) oneCornices) $ \(before, OneCornice h (size -> sz')) -> do
let sz = Sized sz' h
let (beforeSize, cellSize') = compCellSize rowspanAcc (concatMap (map oneColonnadeHead . toList . getColonnade . uncapAnnotated . oneCorniceBody) before) sz
whenIsJust cellSize' $ \cellSize -> do
let Sized{..} = sz
lift . tellM $ fromContent Sized { sizedSize = cellSize, sizedContent }
if | [n] <- mapMaybe (\(key, val) -> guardOnM (is _Rowspan key) $ readMay val) (toSortable sizedContent ^. _sortableContent . cellAttrs)
-> State.modify $ (:) (n, beforeSize, cellSize)
| otherwise -> return ()
let rowspanAcc'' = rowspanAcc'
& traverse . _1 %~ pred
whenIsJust (flattenAnnotated v) $ go rowspanAcc''
compCellSize :: forall h' c. [(Int, Int, Int)] -> [Sized (Maybe Int) h' c] -> Sized (Maybe Int) h' c -> (Int, Maybe Int)
compCellSize rowspanAcc before Sized{..} = (beforeSize,) . assertM' (> 0) $ fromMaybe 1 sizedSize - shadowed
where Sum beforeSize = foldMap (\(Sized sz _) -> Sum $ fromMaybe 1 sz) before
Sum shadowed = flip foldMap rowspanAcc $ \(rowsRem, firstCol, sz) -> fromMaybe mempty $ do
guard $ rowsRem > 0
guard $ firstCol <= beforeSize
guard $ beforeSize < firstCol + sz
return . Sum $ sz - (beforeSize - firstCol)
wrap :: Widget -> Widget
wrap row = case dbsTemplate of
DBSTCourse{} -> row
DBSTDefault{} -> $(widgetFile "table/header")
fromContent :: Sized Int h (DBCell m x) -> WriterT x m Widget
fromContent Sized{ sizedSize = cellSize, sizedContent = toSortable -> Sortable{..} } = do
widget <- sortableContent ^. cellContents
let
cellSize = fromMaybe 1 sizedSize
directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ]
isSortable = isJust sortableKey
isSorted dir = fromMaybe False $ (==) <$> (SortingSetting <$> sortableKey <*> pure dir) <*> listToMaybe psSorting
@ -1280,12 +1318,8 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
case dbsTemplate of
DBSTCourse{} -> return $(widgetFile "table/course/header")
DBSTDefault{} -> return $(widgetFile "table/cell/header")
, case dbsTemplate of
DBSTCourse{} -> id
DBSTDefault{} -> pure . (>>= \row -> return $(widgetFile "table/header")) . foldMapM id
)
in do
wHeaders <- maybe (return Nothing) (fmap Just . foldMapM id . genHeaders) pSortable
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders (dbtColonnade ^. _Cornice)) pSortable
case dbsTemplate of
DBSTCourse c l r s a -> do
wRows <- forM rows $ \row' -> let
@ -1611,3 +1645,27 @@ dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty
genForm _ mkUnique = do
(selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False)
return (set selLens <$> selResult, [whamlet|^{fvWidget selWidget}|])
cap' :: ( AsCornice Sortable p r' (DBCell m x) colonnade
, IsDBTable m x
)
=> colonnade
-> Cornice Sortable ('Cap p) r' (DBCell m x)
cap' (view _Cornice -> cornice) = case cornice of
CorniceBase Colonnade{..}
| [OneColonnade{..}] <- toList getColonnade
-> recap (oneColonnadeHead & _sortableContent . cellAttrs %~ incRowspan) cornice
CorniceCap cornices
-> CorniceCap $ fmap (\OneCornice{..} -> OneCornice { oneCorniceHead = oneCorniceHead & _sortableContent . cellAttrs %~ incRowspan, oneCorniceBody = cap' oneCorniceBody }) cornices
other
-> recap (fromSortable . Sortable Nothing $ cell mempty) other
where
incRowspan :: [(Text, Text)] -> [(Text, Text)]
incRowspan attrs
| [n] <- mapMaybe (\(key, val) -> guardOnM (is _Rowspan key) $ readMay val) attrs
= (_Rowspan # (), tshow (succ n :: Natural)) : filter (hasn't $ _1 . _Rowspan) attrs
| otherwise = (_Rowspan # (), "2") : filter (hasn't $ _1 . _Rowspan) attrs
_Rowspan :: Prism' Text ()
_Rowspan = prism' (\() -> "rowspan") $ flip guardOn () . ((==) `on` CI.mk) "rowspan"

View File

@ -2,10 +2,11 @@
module Handler.Utils.Table.Pagination.Types
( FilterKey(FilterKey), SortingKey(SortingKey)
, Sortable(..)
, Sortable(..), _sortableKey, _sortableContent
, sortable
, ToSortable(..), FromSortable(..)
, SortableP(..)
, IsSortable, _Sortable
, DBTableInvalid(..)
, AsCornice(..)
) where
@ -30,6 +31,8 @@ data Sortable a = Sortable
, sortableContent :: a
}
makeLenses_ ''Sortable
sortable :: Maybe SortingKey -> c -> (a -> c) -> Colonnade Sortable a c
sortable k h = singleton (Sortable k h)
@ -69,6 +72,12 @@ instance FromSortable Headless where
fromSortable _ = Headless
type IsSortable h = (ToSortable h, FromSortable h)
_Sortable :: IsSortable h => Prism' (h a) (Sortable a)
_Sortable = prism' fromSortable $ \x -> ($ x) . toSortable <$> pSortable
data DBTableInvalid = DBTIRowsMissing Int
deriving (Eq, Ord, Read, Show, Generic, Typeable)

View File

@ -55,7 +55,7 @@ import Control.Arrow as Utils ((>>>))
import Control.Monad.Trans.Except (ExceptT(..), throwE, runExceptT)
import Control.Monad.Except (MonadError(..))
import Control.Monad.Trans.Maybe as Utils (MaybeT(..))
import Control.Monad.Trans.Writer.Lazy (execWriterT, tell)
import Control.Monad.Trans.Writer.Lazy (WriterT, execWriterT, tell)
import Control.Monad.Catch
import Control.Monad.Morph (hoist)
import Control.Monad.Fail
@ -802,6 +802,9 @@ diffTimeout timeoutLength timeoutRes act = fromMaybe timeoutRes <$> timeout time
= let (MkFixed micro :: Micro) = realToFrac timeoutLength
in fromInteger micro
tellM :: (Monad m, Monoid x) => m x -> WriterT x m ()
tellM = tell <=< lift
-------------
-- Conduit --
-------------

View File

@ -31,7 +31,7 @@ extra-deps:
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/xss-sanitize.git
commit: 074ed7c8810aca81f60f2c535f9e7bad67e9d95a
- git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
commit: 65164334e9704afc24603a9f3197b4581c996ad8
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
subdirs:
- colonnade

View File

@ -144,12 +144,12 @@ packages:
git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
pantry-tree:
size: 481
sha256: c7137405813404f4e0d2334d67876ab7150e7dc7f8b9f23ad452c5ee76ce4737
commit: 65164334e9704afc24603a9f3197b4581c996ad8
sha256: 392393652cc0f354d351482557b9385c8e6122e706359b030373656565f2e045
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
original:
subdir: colonnade
git: git@gitlab2.rz.ifi.lmu.de:uni2work/colonnade.git
commit: 65164334e9704afc24603a9f3197b4581c996ad8
commit: f8170266ab25b533576e96715bedffc5aa4f19fa
- completed:
hackage: hsass-0.8.0@sha256:82d55fb2a10342accbc4fe80d263163f40a138d8636e275aa31ffa81b14abf01,2792
pantry-tree:

View File

@ -1,5 +1,5 @@
$newline never
<th .table__th *{attrs} :isSortable:.sortable :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc uw-hide-column-header=#{maybe "" toPathPiece sortableKey} :cellSize /= 1:colspan=#{cellSize}>
<th .table__th *{attrs} :isSortable:.sortable :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc :is _Just sortableKey:uw-hide-column-header=#{maybe "" toPathPiece sortableKey} :cellSize /= 1:colspan=#{cellSize}>
$maybe flag <- sortableKey
$case directions
$of [SortAsc]