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 padding-bottom: 10px
font-weight: bold font-weight: bold
text-align: left text-align: left
vertical-align: middle
a a
color: white color: white

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import { Utility } from '../../core/utility'; import { Utility } from '../../core/utility';
import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager'; import { StorageManager, LOCATION } from '../../lib/storage-manager/storage-manager';
import './hide-columns.sass'; import './hide-columns.sass';
import * as memoize from 'lodash.memoize';
const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns'; const HIDE_COLUMNS_CONTAINER_IDENT = 'uw-hide-columns';
const TABLE_HEADER_IDENT = 'uw-hide-column-header'; const TABLE_HEADER_IDENT = 'uw-hide-column-header';
const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label'; const HIDE_COLUMNS_HIDER_LABEL = 'uw-hide-columns--hider-label';
const HIDE_COLUMNS_NO_HIDE = 'uw-hide-columns--no-hide'; 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_ATTR = 'table-utils';
const TABLE_UTILS_CONTAINER_SELECTOR = `[${TABLE_UTILS_ATTR}]`; 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_HIDDEN_CLASS = 'hide-columns--hidden-cell';
const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan'; const CELL_ORIGINAL_COLSPAN = 'uw-hide-column-original-colspan';
const HIDE_COLUMNS_INITIALIZED = 'uw-hide-columns--initialized';
@Utility({ @Utility({
selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`, selector: `[${HIDE_COLUMNS_CONTAINER_IDENT}] table`,
}) })
@ -44,14 +48,15 @@ export class HideColumns {
constructor(element) { constructor(element) {
this._autoHide = this._storageManager.load('autoHide', {}) || false; this._autoHide = this._storageManager.load('autoHide', {}) || false;
if (!element) { if (!element)
throw new Error('Hide Columns utility cannot be setup without an 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 // 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; return false;
}
this._element = element; this._element = element;
@ -74,10 +79,12 @@ export class HideColumns {
this._mutationObserver = new MutationObserver(this._tableMutated.bind(this)); this._mutationObserver = new MutationObserver(this._tableMutated.bind(this));
this._mutationObserver.observe(this._element, { childList: true, subtree: true }); this._mutationObserver.observe(this._element, { childList: true, subtree: true });
this._element.classList.add(HIDE_COLUMNS_INITIALIZED);
} }
setupHideButton(th) { setupHideButton(th) {
const preHidden = this.isHiddenColumn(th); const preHidden = this.isHiddenTH(th);
const hider = document.createElement('span'); const hider = document.createElement('span');
@ -104,20 +111,20 @@ export class HideColumns {
hider.addEventListener('click', (event) => { hider.addEventListener('click', (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.switchColumnDisplay(th, hider); this.switchColumnDisplay(th);
// this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider)); // this._tableHiderContainer.getElementsByClassName(TABLE_HIDER_CLASS).forEach(hider => this.hideHiderBehindHeader(hider));
}); });
hider.addEventListener('mouseover', () => { hider.addEventListener('mouseover', () => {
hider.classList.add(TABLE_HIDER_VISIBLE_CLASS); hider.classList.add(TABLE_HIDER_VISIBLE_CLASS);
const currentlyHidden = this.isHiddenColumn(th); const currentlyHidden = this.hiderStatus(th);
this.updateHiderIcon(hider, !currentlyHidden); this.updateHiderIcon(hider, !currentlyHidden);
}); });
hider.addEventListener('mouseout', () => { hider.addEventListener('mouseout', () => {
if (hider.classList.contains(TABLE_HIDER_CLASS)) { if (hider.classList.contains(TABLE_HIDER_CLASS)) {
hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS); hider.classList.remove(TABLE_HIDER_VISIBLE_CLASS);
} }
const currentlyHidden = this.isHiddenColumn(th); const currentlyHidden = this.hiderStatus(th);
this.updateHiderIcon(hider, currentlyHidden); this.updateHiderIcon(hider, currentlyHidden);
}); });
@ -126,64 +133,76 @@ export class HideColumns {
// reposition hider on each window resize event // reposition hider on each window resize event
// window.addEventListener('resize', () => this.repositionHider(hider)); // window.addEventListener('resize', () => this.repositionHider(hider));
this.updateColumnDisplay(this.colIndex(th), preHidden); this.switchColumnDisplay(th, preHidden);
this.updateHider(hider, preHidden);
if (preHidden) {
this._tableUtilContainer.appendChild(hider);
} else {
this.hideHiderBehindHeader(hider);
}
} }
switchColumnDisplay(th, hider) { switchColumnDisplay(th, hidden) {
const hidden = !this.isHiddenColumn(th); hidden = typeof(hidden) === 'undefined' ? !this.isHiddenTH(th) : !!hidden;
const originalColspan = Math.max(1, th.getAttribute(CELL_ORIGINAL_COLSPAN)) || 1;
const colspan = Math.max(1, th.colSpan) || 1;
const columnIndex = this.colIndex(th);
for (let i = 0; i < Math.max(colspan, originalColspan); i++) { this.cellColumns(th).forEach(columnIndex => this.updateColumnDisplay(columnIndex, hidden));
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);
}
} }
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); const cell = this.getCol(row, columnIndex);
if (cell) { if (cell) {
const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN); const originalColspan = cell.getAttribute(CELL_ORIGINAL_COLSPAN);
const colspan = Math.max(1, cell.colSpan) || 1; 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 (cell.tagName === 'TH') {
if (colspan > 1) { // 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) { if (!originalColspan) {
cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan); cell.setAttribute(CELL_ORIGINAL_COLSPAN, colspan);
} }
cell.colSpan--; cell.colSpan = visibleColumns;
} 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++;
} }
} }
} }
}); });
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) { updateHider(hider, hidden) {
// console.debug({hider, hidden, columnIndex: this.colIndex(this.hiderToHeader.get(hider)), colSpan: this.colSpan(this.hiderToHeader.get(hider))});
if (hidden) { if (hidden) {
hider.classList.remove(TABLE_HIDER_CLASS); hider.classList.remove(TABLE_HIDER_CLASS);
hider.classList.add(TABLE_PILL_CLASS); hider.classList.add(TABLE_PILL_CLASS);
@ -223,12 +242,21 @@ export class HideColumns {
} }
_tableMutated(mutationList) { _tableMutated(mutationList) {
// console.log('_tableMutated', mutationList, observer);
if (!Array.from(mutationList).some(mutation => mutation.type === 'childList')) if (!Array.from(mutationList).some(mutation => mutation.type === 'childList'))
return; 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) { getStorageKey(th) {
@ -257,9 +285,9 @@ export class HideColumns {
} }
isEmptyColumn(columnIndex) { isEmptyColumn(columnIndex) {
for (let row of this._element.getElementsByTagName('tr')) { for (let row of this._element.rows) {
const cell = this.getCol(row, columnIndex); const cell = this.getCol(row, columnIndex);
if (cell.matches('th')) if (!cell || cell.tagName == 'TH')
continue; continue;
if (cell.querySelector('.table__td-content')) { if (cell.querySelector('.table__td-content')) {
for (let child of cell.children) { for (let child of cell.children) {
@ -273,10 +301,47 @@ export class HideColumns {
} }
} }
isHiddenColumn(th) { columnTHs(columnIndex) {
const hidden = this._storageManager.load(this.getStorageKey(th)), return Array.from(this._element.rows)
emptyColumn = this.isEmptyColumn(this.colIndex(th)); .map(row => this.getCol(row, columnIndex))
return hidden === true || hidden === undefined && emptyColumn && this._autoHide; .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) { colSpan(cell) {
@ -289,7 +354,7 @@ export class HideColumns {
return originalColspan ? Math.max(colspan, originalColspan) : colspan; return originalColspan ? Math.max(colspan, originalColspan) : colspan;
} }
colIndex(cell) { colIndexDirect(cell) {
if (!cell) if (!cell)
return 0; return 0;
@ -298,23 +363,62 @@ export class HideColumns {
if (!rowParent) if (!rowParent)
return 0; return 0;
let i = 0; let rowBefore = 0;
for (const sibling of Array.from(rowParent.cells).slice(0, cell.cellIndex)) { for (const cell of Array.from(rowParent.cells).slice(0, cell.cellIndex)) {
i += this.colSpan(sibling); 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; return i;
} }
getCol(row, columnIndex) { _colIndexMemoized;
let c = 0;
for (const cell of row.cells) { colIndex(cell) {
c += cell ? this.colSpan(cell) : 1; 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; 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; 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 CourseParticipantInactive: Abgemeldet
CourseParticipantNoShow: Nicht erschienen CourseParticipantNoShow: Nicht erschienen
CourseUserState: Zustand CourseUserState: Zustand
CourseUserSheets: Übungsblätter
TestDownload: Download-Test TestDownload: Download-Test
TestDownloadMaxSize: Maximale Dateigröße TestDownloadMaxSize: Maximale Dateigröße

View File

@ -2599,6 +2599,7 @@ CourseParticipantActive: Participant
CourseParticipantInactive: Deregistered CourseParticipantInactive: Deregistered
CourseParticipantNoShow: No show CourseParticipantNoShow: No show
CourseUserState: State CourseUserState: State
CourseUserSheets: Exercise sheets
TestDownload: Download test TestDownload: Download test
TestDownloadMaxSize: Maximum filesize 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 module Handler.Course.Users
( queryUser ( queryUser
@ -98,6 +98,7 @@ type UserTableData = DBRow ( Entity User
, ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial))) , ([Entity Tutorial], Map (CI Text) (Maybe (Entity Tutorial)))
, [Entity Exam] , [Entity Exam]
, Maybe (Entity SubmissionGroup) , Maybe (Entity SubmissionGroup)
, Map SheetName (SheetType, Maybe Points)
) )
instance HasEntity UserTableData User where instance HasEntity UserTableData User where
@ -130,13 +131,15 @@ _userExams = _dbrOutput . _6
_userSubmissionGroup :: Traversal' UserTableData (Entity SubmissionGroup) _userSubmissionGroup :: Traversal' UserTableData (Entity SubmissionGroup)
_userSubmissionGroup = _dbrOutput . _7 . _Just _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 :: IsDBTable m c => TermId -> SchoolId -> CourseShorthand -> Colonnade Sortable UserTableData (DBCell m c)
colUserComment tid ssh csh = colUserComment tid ssh csh =
sortable (Just "note") (i18nCell MsgCourseUserNote) sortable (Just "note") (i18nCell MsgCourseUserNote) $ views (_dbrOutput . $(multifocusG 2) (_1 . _entityKey) _3) $ \(uid, mbNoteKey) ->
$ \DBRow{ dbrOutput=(Entity uid _, _, mbNoteKey, _, _, _, _) } -> maybeEmpty mbNoteKey $ const $
maybeEmpty mbNoteKey $ const $ anchorCellM (courseLink <$> encrypt uid) (hasComment True)
anchorCellM (courseLink <$> encrypt uid) (hasComment True)
where where
courseLink = CourseR tid ssh csh . CUserR 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) $ colUserSubmissionGroup = sortable (Just "submission-group") (i18nCell MsgSubmissionGroup) $
foldMap (cell . toWidget) . preview (_userSubmissionGroup . _entityVal . _submissionGroupName) 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 data UserTableCsvStudyFeature = UserTableCsvStudyFeature
{ csvUserField :: Text { csvUserField :: Text
@ -310,14 +327,15 @@ data CourseUserActionData = CourseUserSendMailData
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
makeCourseUserTable :: forall h act act'. makeCourseUserTable :: forall h p cols act act'.
( Functor h, ToSortable h ( Functor h, ToSortable h
, Ord act, PathPiece act, RenderMessage UniWorX act , Ord act, PathPiece act, RenderMessage UniWorX act
, AsCornice h p UserTableData (DBCell (MForm Handler) (FormResult (First act', DBFormResult UserId Bool UserTableData))) cols
) )
=> CourseId => CourseId
-> Map act (AForm Handler act') -> Map act (AForm Handler act')
-> (UserTableExpr -> E.SqlExpr (E.Value Bool)) -> (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)) -> PSValidator (MForm Handler) (FormResult (First act', DBFormResult UserId Bool UserTableData))
-> Maybe (Csv.Name -> Bool) -> Maybe (Csv.Name -> Bool)
-> DB (FormResult (act', Set UserId), Widget) -> 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 dbtProj = traverse $ \(user, participant, E.Value userNoteId, (feature,degree,terms), subGroup) -> do
tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] [] tuts'' <- selectList [ TutorialParticipantUser ==. entityKey user, TutorialParticipantTutorial <-. map entityKey tutorials ] []
exams' <- selectList [ ExamRegistrationUser ==. entityKey user ] [] 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 let
regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials regGroups = setOf (folded . _entityVal . _tutorialRegGroup . _Just) tutorials
tuts' = filter (\(Entity tutId _) -> any ((== tutId) . tutorialParticipantTutorial . entityVal) tuts'') 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' 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 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 dbtColonnade = colChoices
dbtSorting = mconcat dbtSorting = mconcat
[ single $ sortUserNameLink queryUser -- slower sorting through clicking name column header [ 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.&&. courseParticipant E.^. CourseParticipantCourse E.==. submissionGroup E.^. SubmissionGroupCourse
E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId E.on $ submissionGroupUser E.^. SubmissionGroupUserSubmissionGroup E.==. submissionGroup E.^. SubmissionGroupId
E.where_ $ submissionGroup E.^. SubmissionGroupCourse E.==. E.val cid 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 exams = nubOn entityKey $ examOccurrencesPerExam ^.. folded . _1
let colChoices = mconcat $ catMaybes let colChoices = mconcat $ catMaybes
[ pure $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey)) [ pure . cap' $ dbSelect (applying _2) id (return . view (hasEntity . _entityKey))
, pure $ colUserNameLink (CourseR tid ssh csh . CUserR) , pure . cap' $ colUserNameLink (CourseR tid ssh csh . CUserR)
, guardOn showSex $ colUserSex' , guardOn showSex . cap' $ colUserSex'
, pure $ colUserEmail , pure . cap' $ colUserEmail
, pure $ colUserMatriclenr , pure . cap' $ colUserMatriclenr
, pure $ colUserDegreeShort , pure . cap' $ colUserDegreeShort
, pure $ colUserField , pure . cap' $ colUserField
, pure $ colUserSemester , pure . cap' $ colUserSemester
, guardOn hasSubmissionGroups colUserSubmissionGroup , guardOn hasSubmissionGroups $ cap' colUserSubmissionGroup
, guardOn hasTutorials $ colUserTutorials tid ssh csh , guardOn hasTutorials . cap' $ colUserTutorials tid ssh csh
, guardOn hasExams $ colUserExams tid ssh csh , guardOn hasExams . cap' $ colUserExams tid ssh csh
, pure $ sortable (Just "registration") (i18nCell MsgRegisteredSince) (maybe mempty dateCell . preview (_Just . _userTableRegistration) . assertM' (has $ _userTableParticipant . _entityVal . _courseParticipantState . _CourseParticipantActive)) , pure . cap' $ 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 . cap' $ sortable (Just "state") (i18nCell MsgCourseUserState) (i18nCell . view (_userTableParticipant . _entityVal . _courseParticipantState))
, pure $ colUserComment tid ssh csh , guardOn (not $ null sheetList) . colUserSheets $ map (sheetName . entityVal) sheetList
, pure . cap' $ colUserComment tid ssh csh
] ]
psValidator = def & defaultSortingByName psValidator = def & defaultSortingByName
& defaultFilter (singletonMap "active" [toPathPiece True]) & defaultFilter (singletonMap "active" [toPathPiece True])

View File

@ -42,6 +42,7 @@ module Handler.Utils.Table.Pagination
, formCell, DBFormResult(..), getDBFormResult , formCell, DBFormResult(..), getDBFormResult
, dbSelect , dbSelect
, (&) , (&)
, cap'
, module Control.Monad.Trans.Maybe , module Control.Monad.Trans.Maybe
, module Colonnade , module Colonnade
, DBSTemplateMode(..) , DBSTemplateMode(..)
@ -59,6 +60,8 @@ import Utils.Lens
import Import hiding (pi) import Import hiding (pi)
import qualified Data.Foldable as Foldable
import qualified Yesod.Form.Functions as Yesod import qualified Yesod.Form.Functions as Yesod
import qualified Database.Esqueleto as E 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 qualified Network.Wai as Wai
import Control.Monad.RWS (RWST(..), execRWS) 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.Trans.Maybe
import Control.Monad.State.Class (modify) import Control.Monad.State.Class (modify)
import qualified Control.Monad.State.Class as State import qualified Control.Monad.State.Class as State
import Control.Monad.Trans.Writer.Lazy (censor)
import Data.Foldable (Foldable(foldMap))
import Data.Map ((!)) import Data.Map ((!))
import qualified Data.Map as Map import qualified Data.Map as Map
@ -90,7 +92,7 @@ import Colonnade.Encode hiding (row)
import Text.Hamlet (hamletFile) import Text.Hamlet (hamletFile)
import Data.List (elemIndex) import Data.List (elemIndex, inits)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
@ -108,7 +110,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 import qualified Data.Conduit.List as C (sourceList)
import qualified Data.Conduit.Combinators as C
import Handler.Utils.DateTime (formatTimeRangeW) import Handler.Utils.DateTime (formatTimeRangeW)
import qualified Control.Monad.Catch as Catch import qualified Control.Monad.Catch as Catch
@ -1136,7 +1139,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
| otherwise | otherwise
= id = id
in C.sourceList <=< lift . doHandle . runConduit $ dbtCsvComputeActions x .| C.foldMap pure 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' actionMap <- flip evalStateT Map.empty . runConduit $ sourceDiff .| transPipe lift dbtCsvComputeActions'
when (Map.null actionMap) $ do when (Map.null actionMap) $ do
@ -1266,12 +1269,47 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
DBSTDefault{..} -> dbstmNumber rowCount DBSTDefault{..} -> dbstmNumber rowCount
_other -> False _other -> False
genHeaders :: _ -> [WriterT x m Widget] genHeaders :: forall h. Cornice h _ _ (DBCell m x) -> SortableP h -> WriterT x m Widget
genHeaders SortableP{..} = flip (headersMonoidal Nothing) (annotate $ dbtColonnade ^. _Cornice) $ pure genHeaders cornice SortableP{..} = execWriterT . go mempty $ annotate cornice
( \Sized{ sizedSize, sizedContent = toSortable -> Sortable{..} } -> pure $ do 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 widget <- sortableContent ^. cellContents
let let
cellSize = fromMaybe 1 sizedSize
directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ] directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ]
isSortable = isJust sortableKey isSortable = isJust sortableKey
isSorted dir = fromMaybe False $ (==) <$> (SortingSetting <$> sortableKey <*> pure dir) <*> listToMaybe psSorting 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 case dbsTemplate of
DBSTCourse{} -> return $(widgetFile "table/course/header") DBSTCourse{} -> return $(widgetFile "table/course/header")
DBSTDefault{} -> return $(widgetFile "table/cell/header") DBSTDefault{} -> return $(widgetFile "table/cell/header")
, case dbsTemplate of
DBSTCourse{} -> id
DBSTDefault{} -> pure . (>>= \row -> return $(widgetFile "table/header")) . foldMapM id
)
in do 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 case dbsTemplate of
DBSTCourse c l r s a -> do DBSTCourse c l r s a -> do
wRows <- forM rows $ \row' -> let wRows <- forM rows $ \row' -> let
@ -1611,3 +1645,27 @@ dbSelect resLens selLens genIndex = Colonnade.singleton (headednessPure $ mempty
genForm _ mkUnique = do genForm _ mkUnique = do
(selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False) (selResult, selWidget) <- mreq checkBoxField (fsUniq mkUnique "select") (Just False)
return (set selLens <$> selResult, [whamlet|^{fvWidget selWidget}|]) 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 module Handler.Utils.Table.Pagination.Types
( FilterKey(FilterKey), SortingKey(SortingKey) ( FilterKey(FilterKey), SortingKey(SortingKey)
, Sortable(..) , Sortable(..), _sortableKey, _sortableContent
, sortable , sortable
, ToSortable(..), FromSortable(..) , ToSortable(..), FromSortable(..)
, SortableP(..) , SortableP(..)
, IsSortable, _Sortable
, DBTableInvalid(..) , DBTableInvalid(..)
, AsCornice(..) , AsCornice(..)
) where ) where
@ -30,6 +31,8 @@ data Sortable a = Sortable
, sortableContent :: a , sortableContent :: a
} }
makeLenses_ ''Sortable
sortable :: Maybe SortingKey -> c -> (a -> c) -> Colonnade Sortable a c sortable :: Maybe SortingKey -> c -> (a -> c) -> Colonnade Sortable a c
sortable k h = singleton (Sortable k h) sortable k h = singleton (Sortable k h)
@ -69,6 +72,12 @@ instance FromSortable Headless where
fromSortable _ = Headless 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 data DBTableInvalid = DBTIRowsMissing Int
deriving (Eq, Ord, Read, Show, Generic, Typeable) 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.Trans.Except (ExceptT(..), throwE, runExceptT)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
import Control.Monad.Trans.Maybe as Utils (MaybeT(..)) 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.Catch
import Control.Monad.Morph (hoist) import Control.Monad.Morph (hoist)
import Control.Monad.Fail import Control.Monad.Fail
@ -802,6 +802,9 @@ diffTimeout timeoutLength timeoutRes act = fromMaybe timeoutRes <$> timeout time
= let (MkFixed micro :: Micro) = realToFrac timeoutLength = let (MkFixed micro :: Micro) = realToFrac timeoutLength
in fromInteger micro in fromInteger micro
tellM :: (Monad m, Monoid x) => m x -> WriterT x m ()
tellM = tell <=< lift
------------- -------------
-- Conduit -- -- Conduit --
------------- -------------

View File

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

View File

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

View File

@ -1,5 +1,5 @@
$newline never $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 $maybe flag <- sortableKey
$case directions $case directions
$of [SortAsc] $of [SortAsc]