Merge branch 'master' into 409-find-implement-alternative-for-datepicker

This commit is contained in:
Sarah Vaupel 2019-08-13 15:05:24 +02:00
commit 939bbfa884
64 changed files with 1599 additions and 16262 deletions

View File

@ -2,6 +2,76 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [5.0.2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v5.0.1...v5.0.2) (2019-08-13)
### Bug Fixes
* **course-deregister:** only delete relevant users exam results ([3997857](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3997857))
### [5.0.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v5.0.0...v5.0.1) (2019-08-12)
## [5.0.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.14.0...v5.0.0) (2019-08-12)
### Bug Fixes
* removed duplicated code from merge ([9fb9540](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9fb9540))
* **course-teaser:** don't collapse unless chevron is clicked ([fca99be](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/fca99be))
* **course-teaser-css:** class name fixes ([8a92985](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/8a92985))
### Features
* **course-registration:** allow independent course application ([a00698e](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a00698e))
* **course-teaser:** checkbox field for open registration filter ([e4f150d](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e4f150d))
* **course-teaser:** display sorting "pills" for course teasers ([d964e1f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/d964e1f))
* **course-teaser:** filter by open registration ([c2c12b9](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c2c12b9))
* **course-teaser:** final version of course-teaser for course list ([66b97d6](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/66b97d6))
* **course-teaser:** hide lecturer entry if empty ([f7fb3c1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/f7fb3c1))
* **course-teaser:** incomplete course teaser for course list ([9a97925](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/9a97925))
* **course-teaser:** moved course teaser functionality to util ([c99a3c7](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/c99a3c7))
* **course-teaser:** no display of chevron without description ([5c88c13](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/5c88c13))
* **course-teaser:** no page reload on sorting ([68b8d24](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/68b8d24))
* **course-teaser:** only true lecturers without assistants ([7926f29](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7926f29))
* **course-teaser:** redirecting to course/ ([aa20389](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/aa20389))
* **course-teaser:** reintroduced courseId and course-teaser.julius ([3b6e700](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3b6e700))
* **course-teaser:** show openCourses also to logged in users ([8cca548](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/8cca548))
* **course-teaser:** unpolished version of course-teaser for course list ([ea5d54b](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/ea5d54b))
* **course-teaser:** working link to course pages ([8a49979](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/8a49979))
* **course-teaser-css:** removed description label ([a25efb3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/a25efb3))
* **course-teaser-filter:** filter for lecturers ([e96e17f](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/e96e17f))
* **course-teaser-filter:** working filters for semester and institute ([3b419b3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/3b419b3))
* **courses:** rework couse registration ([79d4ae2](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/79d4ae2))
### BREAKING CHANGES
* **courses:** auditing for course registrations and deregistrations, more
tightly couple exam results, exam registration, and course registration (delete
them together now)
## [4.14.0](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.13.1...v4.14.0) (2019-08-07)
### Bug Fixes
* **exam:** fix warning message ([60869fd](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/60869fd))
* **info:** minor whitespace correction ([0ce4dd1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/0ce4dd1))
### Features
* **info:** info seiten überarbeitet ([7459fc3](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/commit/7459fc3))
### [4.13.1](https://gitlab.cip.ifi.lmu.de/jost/UniWorX/compare/v4.13.0...v4.13.1) (2019-08-07)

View File

@ -60,9 +60,9 @@ export class AsyncTable {
this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
}
const table = this._element.querySelector('table');
const table = this._element.querySelector('table, .div__course-teaser');
if (!table) {
throw new Error('Async Table utility needs a <table> in its element!');
throw new Error('Async Table utility needs a <table> or a <div .div__course-teaser> in its element!');
}
const rawTableId = table.id;
@ -94,7 +94,7 @@ export class AsyncTable {
}
_setupSortableHeaders() {
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable'))
this._ths = Array.from(this._scrollTable.querySelectorAll('th.sortable, .course-header'))
.map((th) => ({ element: th }));
this._ths.forEach((th) => {

View File

@ -0,0 +1,37 @@
import { Utility } from '../../core/utility';
import './course-teaser.scss';
var COURSE_TEASER_INITIALIZED_CLASS = 'course-teaser--initialized';
var COURSE_TEASER_EXPANDED_CLASS = 'course-teaser__expanded';
var COURSE_TEASER_CHEVRON_CLASS = 'course-teaser__chevron';
@Utility({
selector: '[uw-course-teaser]:not(.course-teaser__disabled)',
})
export class CourseTeaser {
_element;
constructor(element) {
if (!element) {
throw new Error('CourseTeaser utility cannot be setup without an element!');
}
if (element.classList.contains(COURSE_TEASER_INITIALIZED_CLASS)) {
return false;
}
this._element = element;
element.addEventListener('click', e => this._onToggleExpand(e));
}
_onToggleExpand(event) {
var isLink = event.target.tagName.toLowerCase() === 'a';
var isChevron = event.target.classList.contains(COURSE_TEASER_CHEVRON_CLASS);
var isExpanded = this._element.classList.contains(COURSE_TEASER_EXPANDED_CLASS);
if ((!isExpanded && !isLink) || isChevron) {
this._element.classList.toggle(COURSE_TEASER_EXPANDED_CLASS);
}
}
}

View File

@ -0,0 +1,8 @@
# CourseTeaser
Handles expanding and collapsing single course teaser widgets on click.
## Example usage
```html
<div uw-course-teaser>
```

View File

@ -0,0 +1,181 @@
[uw-course-teaser] {
display: grid;
grid-gap: 5px 7px;
grid-template-columns: 50px 120px 1fr;
padding: 10px;
/* background-color: var(--course-bg-color); */
transition: background-color .1s ease-out;
&:not(.course-teaser__disabled) {
cursor: pointer;
}
&:hover {
background-color: var(--course-bg-color);
}
+ [uw-course-teaser] {
margin-top: 10px;
}
}
/* chevron */
.course-teaser__chevron {
cursor: pointer;
padding: 10px;
grid-column: 1;
grid-row: 2;
justify-self: center;
align-self: center;
&::before {
content: '';
display: block;
margin-top: -8px;
border-width: 0 3px 3px 0;
width: 8px;
height: 8px;
border-color: var(--color-fontsec);
border-style: solid;
transform: rotate(45deg);
transition: transform .2s ease-out;
}
}
/* semester */
.course-teaser__semester {
grid-column: 2;
grid-row: 1;
justify-self: end;
font-size: 1.1rem;
a {
color: var(--color-fontsec);
}
}
/* shorthand */
.course-teaser__shorthand {
grid-column: 2;
grid-row: 2;
justify-self: end;
font-size: 1.2rem;
font-weight: bold;
overflow-wrap: anywhere;
}
/* title */
.course-teaser__title {
grid-column: 3;
grid-row: 2;
font-size: 1.2rem;
align-self: baseline;
}
/* registration */
.course-teaser__registration {
grid-column: 4;
grid-row: 2;
justify-self: end;
align-self: baseline;
color: var(--color-fontsec);
}
/* school */
.course-teaser__school-value {
grid-column: 3;
grid-row: 1;
align-self: end;
a {
color: var(--color-fontsec);
}
}
/* description */
.course-teaser__description {
grid-column: 2 / 4;
max-height: 1000px;
overflow: auto;
/* color: var(--color-fontsec); */
}
/* subtitle */
.course-teaser__lecturer-label,
.course-teaser__duedate-label,
.course-teaser__school-label {
grid-column: 2;
justify-self: end;
color: var(--color-fontsec);
font-style: italic;
}
/* hidden in closed state */
.course-teaser__description,
.course-teaser__registration {
display: none;
}
/* registered courses */
.course-teaser__registered {
.course-teaser__registration {
display: block;
}
}
/* expanded courses */
.course-teaser__expanded {
.course-teaser__chevron::before {
transform: translateY(7px) rotate(225deg);
}
.course-teaser__school-label,
.course-teaser__school-value,
.course-teaser__duedate-label,
.course-teaser__duedate-value,
.course-teaser__description {
display: block;
}
}
/*
course teaser: header styling
*/
.course-teaser-header {
padding-top: 10px;
padding-bottom: 20px;
line-height: 1.4;
max-width: 85vw;
.course-header {
float: left;
background-color: var(--color-dark);
position: relative;
font-size: 16px;
color: #fff;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 35px;
margin-bottom: 10px;
font-weight: bold;
text-align: left;
border-radius: 20px 20px 20px 20px / 50% 50% 50% 50%;
margin-right: 30px;
.course-header-link {
color: white;
font-weight: bold;
text-decoration: none;
&:hover {
color: inherit;
}
}
}
}
.course-teaser-header:after {
content: "";
display: table;
clear: both;
}

View File

@ -9,6 +9,7 @@ import { InputUtils } from './inputs/inputs';
import { MassInput } from './mass-input/mass-input';
import { Modal } from './modal/modal';
import { Tooltip } from './tooltips/tooltips';
import { CourseTeaser } from './course-teaser/course-teaser';
export const Utils = [
Alerts,
@ -23,4 +24,5 @@ export const Utils = [
Modal,
ShowHide,
Tooltip,
CourseTeaser,
];

View File

@ -7,8 +7,10 @@ BtnRegister: Anmelden
BtnDeregister: Abmelden
BtnCourseRegister: Zum Kurs anmelden
BtnCourseDeregister: Vom Kurs abmelden
BtnExamRegister: Anmelden zur Klausur
BtnExamDeregister: Von der Klausur abmelden
BtnCourseApply: Zum Kurs bewerben
BtnCourseRetractApplication: Bewerbung zum Kurs zurückziehen
BtnExamRegister: Anmelden zur Prüfung
BtnExamDeregister: Von der Prüfung abmelden
BtnHijack: Sitzung übernehmen
BtnSave: Speichern
PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert.
@ -86,15 +88,19 @@ CourseCapacityTip: Anzahl erlaubter Kursanmeldungen, leer lassen für unbeschrä
CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegisterOk: Anmeldung erfolgreich
CourseDeregisterOk: Erfolgreich abgemeldet
CourseRegistration: Kursanmeldung
CourseRegisterOpen: Anmeldung möglich
CourseRegisterOk: Erfolgreich zum Kurs angemeldet
CourseDeregisterOk: Erfolgreich vom Kurs abgemeldet
CourseApplyOk: Erfolgreich zum Kurs beworben
CourseRetractApplyOk: Bewerbung zum Kurs erfolgreich zurückgezogen
CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
CourseStudyFeature: Assoziiertes Hauptfach
CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert
CourseStudyFeatureNone: Kein assoziiertes Hauptfach
CourseStudyFeature: Assoziiertes Studienfach
CourseStudyFeatureTip: Dient ausschließlich der Information der Kursverwalter
CourseStudyFeatureUpdated: Assoziiertes Studienfach geändert
CourseStudyFeatureNone: Kein assoziiertes Studienfach
CourseTutorial: Tutorium
CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen
CourseSecretWrong: Falsches Kennwort
CourseSecretWrong: Falsches Passwort
CourseSecret: Zugangspasswort
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert.
CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester.
@ -112,7 +118,7 @@ CourseMembers: Teilnehmer
CourseMemberOf: Teilnehmer
CourseMembersCount n@Int: #{n}
CourseMembersCountLimited n@Int max@Int: #{n}/#{max}
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Anmeldungen #{maybeToMessage " von " mbNum " möglichen"}
CourseMembersCountOf n@Int mbNum@IntMaybe: #{n} Kursanmeldungen #{maybeToMessage " von " mbNum " möglichen"}
CourseName: Name
CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
@ -136,8 +142,8 @@ CourseUserNote: Notiz
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
CourseUserNoteSaved: Notizänderungen gespeichert
CourseUserNoteDeleted: Teilnehmernotiz gelöscht
CourseUserDeregister: Abmelden
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet
CourseUserDeregister: Vom Kurs abmelden
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
CourseUserSendMail: Mitteilung verschicken
TutorialUserDeregister: Vom Tutorium Abmelden
TutorialUserSendMail: Mitteilung verschicken
@ -149,20 +155,48 @@ CourseAllocationOption term@Text name@Text: #{name} (#{term})
CourseAllocationMinCapacity: Minimale Teilnehmeranzahl
CourseAllocationMinCapacityTip: Wenn der Veranstaltung bei der Zentralanmeldung weniger als diese Anzahl von Teilnehmern zugeteilt würden, werden diese stattdessen auf andere Kurse umverteilt
CourseAllocationMinCapacityMustBeNonNegative: Minimale Teilnehmeranzahl darf nicht negativ sein
CourseAllocationInstructions: Anweisungen zur Bewerbung
CourseAllocationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben
CourseAllocationApplicationTemplate: Bewerbungsvorlagen
CourseAllocationApplicationText: Text-Bewerbungen
CourseAllocationApplicationTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen?
CourseAllocationApplicationRatingsVisible: Feedback für Bewerbungen
CourseAllocationApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden?
CourseApplicationInstructions: Anweisungen zur Bewerbung/Anmeldung
CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseApplicationTemplate: Bewerbungsvorlagen
CourseApplicationTemplateTip: Werden den Studierenden zum download angeboten, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseApplicationsText: Text-Bewerbungen
CourseApplicationsTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen?
CourseApplicationRatingsVisible: Feedback für Bewerbungen
CourseApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden?
CourseApplicationRequired: Bewerbungsverfahren
CourseApplicationRequiredTip: Sollen Anmeldungen zu diesem Kurs zunächst provisorisch (ohne Kapazitätsbeschränkung) sein, bis sie durch einen Kursverwalter (nach Bewertung der Bewerbungen) akzeptiert werden?
CourseApplicationInstructionsApplication: Anweisungen zur Bewerbung
CourseApplicationInstructionsRegistration: Anweisungen zur Anmeldung
CourseApplicationTemplateApplication: Bewerbungsvorlage(n)
CourseApplicationTemplateRegistration: Anmeldungsvorlage(n)
CourseApplicationTemplateArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-bewerbungsvorlagen
CourseApplicationText: Text-Bewerbung
CourseApplicationFollowInstructions: Beachten Sie die Anweisungen zur Bewerbung!
CourseRegistrationText: Text zur Anmeldung
CourseRegistrationFollowInstructions: Beachten Sie die Anweisungen zur Anmeldung!
CourseApplicationFile: Bewerbung
CourseApplicationFiles: Bewerbungsdatei(en)
CourseApplicationArchive: Zip-Archiv der Bewerbungsdatei(en)
CourseRegistrationFile: Datei zur Anmeldung
CourseRegistrationFiles: Datei(en) zur Anmeldung
CourseRegistrationArchive: Zip-Archiv der Datei(en) zur Anmeldung
CourseApplicationNoFiles: Keine Datei(en)
CourseApplicationDeleteToEdit: Um Ihre Bewerbung zu editieren müssen Sie sie zunächst zurückziehen und sich erneut bewerben.
CourseRegistrationDeleteToEdit: Um Ihre Anmeldungsdaten zu editieren müssen Sie sich zunächst ab- und dann erneut anmelden.
CourseLoginToApply: Um sich zum Kurz zu bewerben müssen Sie sich zunächst in Uni2work anmelden
CourseLoginToRegister: Um sich zum Kurs anzumelden müssen Sie zunächst in Uni2work anmelden
CourseApplicationArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand appId@CryptoFileNameCourseApplication displayName@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldCase (toPathPiece appId)}-#{foldCase displayName}
CourseNoAllocationsAvailable: Es sind aktuell keine Zentralanmeldungen verfügbar
AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden
CourseFormSectionRegistration: Anmeldung
CourseFormSectionRegistration: Anmeldung zum Kurs
CourseFormSectionAdministration: Verwaltung
CourseLecturers: Kursverwalter
@ -183,6 +217,8 @@ NoSuchSchool ssh@SchoolId: Institut #{ssh} gibt es nicht.
NoSuchCourseShorthand csh@CourseShorthand: Kein Kurs mit Kürzel #{csh} bekannt.
NoSuchCourse: Keinen passenden Kurs gefunden.
NoCourseDescription: Zu diesem Kurs ist keine Beschreibung verfügbar.
Sheet: Blatt
SheetList tid@TermId ssh@SchoolId csh@CourseShorthand: #{tid}-#{ssh}-#{csh} Übersicht Übungsblätter
SheetNewHeading tid@TermId ssh@SchoolId csh@CourseShorthand: #{tid}-#{ssh}-#{csh} Neues Übungsblatt anlegen
@ -290,11 +326,13 @@ MaterialDeleteCaption: Wollen Sie das unten aufgeführte Material wirklich lösc
MaterialDelHasFiles count@Int64: inklusive #{count} #{pluralDE count "Datei" "Dateien"}
MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht.
MaterialDeleted materialName@MaterialName: Material "#{materialName}" gelöscht
MaterialArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand materialName@MaterialName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase materialName}
Unauthorized: Sie haben hierfür keine explizite Berechtigung.
UnauthorizedAnd l@Text r@Text: (#{l} UND #{r})
UnauthorizedOr l@Text r@Text: (#{l} ODER #{r})
UnauthorizedNot i@Text: (NICHT #{i})
UnauthorizedNoToken: Ihrer Anfrage war kein Authorisierungs-Token beigefügt.
UnauthorizedTokenExpired: Ihr Authorisierungs-Token ist abgelaufen.
UnauthorizedTokenNotStarted: Ihr Authorisierungs-Token ist noch nicht gültig.
@ -311,12 +349,14 @@ UnauthorizedCorrector: Sie sind nicht als Korrektor für diese Veranstaltung ein
UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen.
UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert.
UnauthorizedExamResult: Sie haben keine Ergebnisse in dieser Prüfung.
UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
UnauthorizedApplicationTime: Diese Bewerbung ist momentan nicht freigegeben.
UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben.
UnauthorizedTutorialTime: Dieses Tutorium erlaubt momentan keine Anmeldungen.
UnauthorizedExamTime: Diese Klausur ist momentan nicht freigegeben.
UnauthorizedExamTime: Diese Prüfung ist momentan nicht freigegeben.
UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert.
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
@ -395,7 +435,7 @@ TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenCourses: Kurse mit offener Registrierung
HomeUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Klausuren
HomeUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} Kurse
CloseAlert: Schliessen
@ -445,6 +485,10 @@ UpdatedSheetCorrectorsAutoFailed n@Int: #{n} #{pluralDE n "Abgabe konnte" "Abgab
CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden:
SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
SubmissionOriginal: Original
SubmissionCorrected: Korrigiert
SubmissionArchiveName: abgaben
SubmissionTypeArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName subId@CryptoFileNameSubmission renderedSfType@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase (toPathPiece subId)}-#{foldCase renderedSfType}
CorrectionSheets: Übersicht Korrekturen nach Blättern
CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren
@ -532,7 +576,7 @@ MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufget
NoTableContent: Kein Tabelleninhalt
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter
NoUpcomingExams: In den nächsten 14 Tagen gibt es keine Klausur mit offener Registrierung in Ihren Kursen
NoUpcomingExams: In den nächsten 14 Tagen gibt es keine Prüfung mit offener Registrierung in Ihren Kursen
AdminHeading: Administration
AdminUserHeading: Benutzeradministration
@ -550,6 +594,8 @@ ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"}
Date: Datum
DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat
@ -642,7 +688,7 @@ StudyFeatureAge: Fachsemester
StudyFeatureDegree: Abschluss
FieldPrimary: Hauptfach
FieldSecondary: Nebenfach
NoPrimaryStudyField: (kein Hauptfach registriert)
NoStudyField: Kein Studienfach
StudyFeatureType:
StudyFeatureValid: Aktiv
StudyFeatureUpdate: Abgeglichen
@ -770,6 +816,9 @@ SheetGroupMaxGroupsize: Maximale Gruppengröße
SheetFiles: Übungsblatt-Dateien
SheetFileTypeHeader: Zugehörigkeit
SheetArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}
SheetTypeArchiveName tid@TermId ssh@SchoolId csh@CourseShorthand shn@SheetName renderedSft@Text: #{foldCase (termToText (unTermKey tid))}-#{foldedCase (unSchoolKey ssh)}-#{foldedCase csh}-#{foldedCase shn}-#{foldCase renderedSft}
NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übungsblatt wurde korrigiert
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen
@ -779,11 +828,11 @@ NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugetei
NotificationTriggerCorrectionsNotDistributed: Nicht alle Abgaben eines meiner Übungsblätter konnten einem Korrektor zugeteilt werden
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
NotificationTriggerUserAuthModeUpdate: Mein Anmelde-Modus wurde geändert
NotificationTriggerExamResult: Ich kann ein neues Klausurergebnis einsehen
NotificationTriggerExamResult: Ich kann ein neues Prüfungsergebnis einsehen
NotificationTriggerKindAll: Für alle Benutzer
NotificationTriggerKindCourseParticipant: Für Kursteilnehmer
NotificationTriggerKindExamParticipant: Für Klausurteilnehmer
NotificationTriggerKindExamParticipant: Für Prüfungsteilnehmer
NotificationTriggerKindCorrector: Für Korrektoren
NotificationTriggerKindLecturer: Für Dozenten
NotificationTriggerKindAdmin: Für Administratoren
@ -945,11 +994,11 @@ MenuAuthPreds: Authorisierungseinstellungen
MenuTutorialDelete: Tutorium löschen
MenuTutorialEdit: Tutorium editieren
MenuTutorialComm: Mitteilung an Teilnehmer
MenuExamList: Klausuren
MenuExamNew: Neue Klausur anlegen
MenuExamList: Prüfungen
MenuExamNew: Neue Prüfung anlegen
MenuExamEdit: Bearbeiten
MenuExamUsers: Teilnehmer
MenuExamAddMembers: Klausurteilnehmer hinzufügen
MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
MenuLecturerInvite: Dozenten hinzufügen
AuthPredsInfo: Um eigene Veranstaltungen aus Sicht der Teilnehmer anzusehen, können Veranstalter und Korrektoren hier die Prüfung ihrer erweiterten Berechtigungen temporär deaktivieren. Abgewählte Prädikate schlagen immer fehl. Abgewählte Prädikate werden also nicht geprüft um Zugriffe zu gewähren, welche andernfalls nicht erlaubt wären. Diese Einstellungen gelten nur temporär bis Ihre Sitzung abgelaufen ist, d.h. bis ihr Browser-Cookie abgelaufen ist. Durch Abwahl von Prädikaten kann man sich höchstens temporär aussperren.
@ -968,7 +1017,8 @@ AuthTagTime: Zeitliche Einschränkungen sind erfüllt
AuthTagAllocationTime: Zeitliche Einschränkungen durch Zentralanmeldung sind erfüllt
AuthTagCourseRegistered: Nutzer ist Kursteilnehmer
AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
AuthTagExamRegistered: Nutzer ist Klausurteilnehmer
AuthTagExamRegistered: Nutzer ist Prüfungsteilnehmer
AuthTagExamResult: Nutzer hat Prüfungsergebnisse
AuthTagParticipant: Nutzer ist mit Kurs assoziiert
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
AuthTagCapacity: Kapazität ist ausreichend
@ -1047,12 +1097,12 @@ TutorInviteExplanation: Sie wurden eingeladen, Tutor zu sein.
ExamCorrectorInvitationAccepted examn@ExamName: Sie wurden als Korrektor für #{examn} eingetragen
ExamCorrectorInvitationDeclined examn@ExamName: Sie haben die Einladung, Korrektor für #{examn} zu werden, abgelehnt
ExamCorrectorInviteHeading examn@ExamName: Einladung zum Korrektor für #{examn}
ExamCorrectorInviteExplanation: Sie wurden eingeladen, Klausur-Korrektor zu sein.
ExamCorrectorInviteExplanation: Sie wurden eingeladen, Prüfungs-Korrektor zu sein.
ExamRegistrationInvitationAccepted examn@ExamName: Sie wurden als Teilnehmer für #{examn} eingetragen
ExamRegistrationInvitationDeclined examn@ExamName: Sie haben die Einladung, Teilnehmer für #{examn} zu werden, abgelehnt
ExamRegistrationInviteHeading examn@ExamName: Einladung zum Teilnehmer für #{examn}
ExamRegistrationInviteExplanation: Sie wurden eingeladen, Klausurteilnehmer zu sein.
ExamRegistrationInviteExplanation: Sie wurden eingeladen, Prüfungsteilnehmer zu sein.
SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen
SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
@ -1102,7 +1152,7 @@ TutorialsHeading: Tutorien
TutorialEdit: Bearbeiten
TutorialDelete: Löschen
CourseExams: Klausuren
CourseExams: Prüfungen
CourseTutorials: Übungen
ParticipantsN n@Int: #{n} Teilnehmer
@ -1146,26 +1196,26 @@ HealthActiveJobExecutors: Anteil der job-workers, die neue Befehle annehmen
CourseParticipants n@Int: Derzeit #{n} angemeldete Kursteilnehmer
CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
CourseParticipantsAlreadyRegistered n@Int: #{n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
CourseParticipantsRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Hauptfach" "wurden ohne assoziierte Hauptfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}
CourseParticipantsRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Studienfach" "wurden ohne assoziierte Studienfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}
CourseParticipantsRegistered n@Int: #{n} Teilnehmer erfolgreich angemeldet
CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen
ExamRegistrationAndCourseParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} sowohl zum Kurs, als auch zur Klausur angemeldet
ExamRegistrationNotRegisteredWithoutCourse n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} nicht zur Klausur angemeldet, da #{pluralDE n "er" "sie"} nicht zum Kurs angemeldet #{pluralDE n "ist" "sind"}
ExamRegistrationRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} sowohl zur Klausur, als auch #{pluralDE n "ohne assoziiertes Hauptfach" "ohne assoziierte Hauptfächer"} zum Kurs angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}
ExamRegistrationParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} zur Klausur angemeldet
ExamRegistrationAndCourseParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} sowohl zum Kurs, als auch zur Prüfung angemeldet
ExamRegistrationNotRegisteredWithoutCourse n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} nicht zur Prüfung angemeldet, da #{pluralDE n "er" "sie"} nicht zum Kurs angemeldet #{pluralDE n "ist" "sind"}
ExamRegistrationRegisteredWithoutField n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} sowohl zur Prüfung, als auch #{pluralDE n "ohne assoziiertes Studienfach" "ohne assoziierte Studienfächer"} zum Kurs angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}
ExamRegistrationParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} zur Prüfung angemeldet
ExamRegistrationInviteDeadline: Einladung nur gültig bis
ExamRegistrationEnlistDirectly: Bekannte Nutzer sofort als Teilnehmer eintragen
ExamRegistrationEnlistDirectlyTip: Sollen, wenn manche der E-Mail Addressen bereits in Uni2work mit Nutzern assoziiert sind, jene Nutzer direkt zur Klausur hinzugefügt werden? Ansonsten werden Einladung an alle E-Mail Addressen (nicht nur unbekannte) versandt, die die Nutzer zunächst akzeptieren müssen um Klausurteilnehmer zu werden.
ExamRegistrationEnlistDirectlyTip: Sollen, wenn manche der E-Mail Addressen bereits in Uni2work mit Nutzern assoziiert sind, jene Nutzer direkt zur Prüfung hinzugefügt werden? Ansonsten werden Einladung an alle E-Mail Addressen (nicht nur unbekannte) versandt, die die Nutzer zunächst akzeptieren müssen um Prüfungsteilnehmer zu werden.
ExamRegistrationRegisterCourse: Nutzer auch zum Kurs anmelden
ExamRegistrationRegisterCourseTip: Nutzer, die keine Kursteilnehmer sind, werden sonst nicht zur Klausur angemeldet.
ExamRegistrationRegisterCourseTip: Nutzer, die keine Kursteilnehmer sind, werden sonst nicht zur Prüfung angemeldet.
ExamRegistrationInviteField: Einzuladende EMail Addressen
ExamParticipantsRegisterHeading: Klausurteilnehmer hinzufügen
ExamParticipantsRegisterHeading: Prüfungsteilnehmer hinzufügen
ExamParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
ExamName: Name
ExamTime: Termin
ExamsHeading: Klausuren
ExamsHeading: Prüfungen
ExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein
ExamStart: Beginn
ExamEnd: Ende
@ -1173,7 +1223,7 @@ ExamDescription: Beschreibung
ExamVisibleFrom: Sichtbar ab
ExamVisibleFromTip: Ohne Datum nie sichtbar und keine Anmeldung möglich
ExamRegisterFrom: Anmeldung ab
ExamRegisterFromTip: Zeitpunkt ab dem sich Kursteilnehmer selbständig zur Klausur anmelden können; ohne Datum ist keine Anmeldung möglich
ExamRegisterFromTip: Zeitpunkt ab dem sich Kursteilnehmer selbständig zur Prüfung anmelden können; ohne Datum ist keine Anmeldung möglich
ExamRegisterTo: Anmeldung bis
ExamDeregisterUntil: Abmeldung bis
ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um
@ -1181,7 +1231,7 @@ ExamPublishOccurrenceAssignmentsTip: Ab diesem Zeitpunkt Teilnehmer einsehen zu
ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen
ExamFinishedTip: Zeitpunkt zu dem Klausurergebnisse den Teilnehmern gemeldet werden
ExamFinishedTip: Zeitpunkt zu dem Prüfungergebnisse den Teilnehmern gemeldet werden
ExamClosed: Noten stehen fest ab
ExamClosedTip: Zeitpunkt ab dem keine Änderungen an den Ergebnissen zulässig sind; Prüfungsämter bekommen Einsicht
ExamShowGrades: Noten anzeigen
@ -1197,15 +1247,15 @@ Points: Punkte
PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein
PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
GradingFrom: Ab
ExamNew: Neue Klausur
ExamBonusRule: Klausurbonus aus Übungsbetrieb
ExamNew: Neue Prüfung
ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten
ExamEditHeading examn@ExamName: #{examn} bearbeiten
ExamBonusMaxPoints: Maximal erreichbare Klausur-Bonuspunkte
ExamBonusMaxPointsNonPositive: Maximaler Klausurbonus muss positiv und größer null sein
ExamBonusMaxPoints: Maximal erreichbare Prüfungs-Bonuspunkte
ExamBonusMaxPointsNonPositive: Maximaler Prüfungsbonus muss positiv und größer null sein
ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen
ExamOccurrenceRule: Automatische Termin- bzw. Raumzuteilung
@ -1238,7 +1288,7 @@ ExamFormCorrection: Korrektur
ExamFormParts: Teile
ExamCorrectors: Korrektoren
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Klausur eingetragen
ExamCorrectorAlreadyAdded: Ein Korrektor mit dieser E-Mail ist bereits für diese Prüfung eingetragen
ExamParts: Teilaufgaben
ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein
@ -1248,30 +1298,30 @@ ExamPartMaxPoints: Maximalpunktzahl
ExamPartWeight: Gewichtung
ExamPartResultPoints: Erreichte Punkte
ExamNameTaken exam@ExamName: Es existiert bereits eine Klausur mit Namen #{exam}
ExamNameTaken exam@ExamName: Es existiert bereits eine Prüfung mit Namen #{exam}
ExamCreated exam@ExamName: #{exam} erfolgreich angelegt
ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
ExamNoShow: Nicht erschienen
ExamVoided: Entwertet
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Klausurpunkte
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Klausurpunkte, falls die Klausur auch ohne Bonus bereits bestanden ist
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist
ExamPassed: Bestanden
ExamNotPassed: Nicht bestanden
ExamResult: Klausurergebnis
ExamResult: Prüfungsergebnis
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur #{exam} angemeldet
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der #{exam} abgemeldet
ExamRegistered: Angemeldet
ExamNotRegistered: Nicht angemeldet
ExamRegistration: Anmeldung
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur Prüfung #{exam} angemeldet
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Prüfung #{exam} abgemeldet
ExamRegistered: Zur Prüfung angemeldet
ExamNotRegistered: Nicht zur Prüfung angemeldet
ExamRegistration: Prüfungsanmeldung
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Termin- bzw. Raumzuordnung liegen
ExamEndMustBeAfterStart: Beginn der Klausur muss vor ihrem Ende liegen
ExamEndMustBeAfterStart: Beginn der Prüfung muss vor ihrem Ende liegen
ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" liegen
@ -1279,18 +1329,19 @@ ExamClosedMustBeAfterStart: "Noten stehen fest ab" muss nach Start liegen
ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem Ende liegen
ExamOccurrenceStartMustBeAfterExamStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss nach Beginn der Klausur liegen
ExamOccurrenceEndMustBeBeforeExamEnd eoName@ExamOccurrenceName: Ende des Termins #{eoName} muss vor Ende der Klausur liegen
ExamOccurrenceStartMustBeAfterExamStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss nach Beginn der Prüfung liegen
ExamOccurrenceEndMustBeBeforeExamEnd eoName@ExamOccurrenceName: Ende des Termins #{eoName} liegt nach dem Ende der Prüfung
ExamOccurrenceDuplicate eoRoom@Text eoRange@Text: Raum #{eoRoom}, Termin #{eoRange} kommt mehrfach mit der selben Beschreibung vor
ExamOccurrenceDuplicateName eoName@ExamOccurrenceName: Interne Terminbezeichnung #{eoName} kommt mehrfach vor
VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs
ImplementationDetails: Implementierung
ExamUsersHeading: Klausurteilnehmer
ExamUserDeregister: Teilnehmer von Klausur abmelden
ExamUsersHeading: Prüfungsteilnehmer
ExamUserDeregister: Teilnehmer von Prüfung abmelden
ExamUserAssignOccurrence: Termin/Raum zuweisen
ExamUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet
ExamUsersDeregistered count@Int64: #{show count} Teilnehmer von der Prüfung abgemeldet
ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt
CsvFile: CSV-Datei
@ -1316,15 +1367,15 @@ CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
CsvColumnExamUserMatriculation: Matrikelnummer des Teilnehmers
CsvColumnExamUserField: Hauptfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnExamUserDegree: Abschluss, den der Teilnehmer im assoziierten Hauptfach anstrebt
CsvColumnExamUserSemester: Fachsemester des Teilnehmers im assoziierten Hauptfach
CsvColumnExamUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnExamUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt
CsvColumnExamUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
CsvColumnExamUserOccurrence: Prüfungstermin/-Raum, zu dem der Teilnehmer angemeldet ist
CsvColumnExamUserExercisePoints: Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb erreicht hat
CsvColumnExamUserExercisePointsMax: Maximale Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb bis zu seinem Klausurtermin erreichen hätte können
CsvColumnExamUserExercisePointsMax: Maximale Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb bis zu seinem Prüfungstermin erreichen hätte können
CsvColumnExamUserExercisePasses: Anzahl von Übungsblättern, die der Teilnehmer bestanden hat
CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Klausurtermin bestehen hätte können
CsvColumnExamUserResult: Erreichte Klausurleistung; "passed", "failed", "no-show", "voided", oder eine Note ("1.0", "1.3", "1.7", ..., "4.0", "5.0")
CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Prüfungstermin bestehen hätte können
CsvColumnExamUserResult: Erreichte Prüfungsleistung; "passed", "failed", "no-show", "voided", oder eine Note ("1.0", "1.3", "1.7", ..., "4.0", "5.0")
CsvColumnExamUserCourseNote: Notizen zum Teilnehmer
Action: Aktion
@ -1334,18 +1385,18 @@ DBCsvDuplicateKeyTip: Entfernen Sie eine der unten aufgeführten Zeilen aus Ihre
DBCsvKeyException: Für eine Zeile der CSV-Dateien konnte nicht festgestellt werden, ob sie zu einem bestehenden internen Datensatz korrespondieren.
DBCsvException: Bei der Berechnung der auszuführenden Aktionen für einen Datensatz ist ein Fehler aufgetreten.
ExamUserCsvCourseRegister: Benutzer zum Kurs und zur Klausur anmelden
ExamUserCsvRegister: Kursteilnehmer zur Klausur anmelden
ExamUserCsvCourseRegister: Benutzer zum Kurs und zur Prüfung anmelden
ExamUserCsvRegister: Kursteilnehmer zur Prüfung anmelden
ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen
ExamUserCsvDeregister: Teilnehmer von der Klausur abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Hauptfach ändern
ExamUserCsvDeregister: Teilnehmer von der Prüfung abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Studienfach ändern
ExamUserCsvSetResult: Ergebnis eintragen
ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen
ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht
ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identifiziert werden
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Hauptfach des Kursteilnehmers zugeordnet werden
ExamUserCsvExceptionNoMatchingStudyFeatures: Das angegebene Studienfach konnte keinem Studienfach des Kursteilnehmers zugeordnet werden
ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
TableHeadingFilter: Filter
@ -1355,7 +1406,7 @@ TableHeadingCsvExport: CSV-Export
ExamResultAttended: Teilgenommen
ExamResultNoShow: Nicht erschienen
ExamResultVoided: Entwertet
ExamResultNone: Kein Klausurergebnis
ExamResultNone: Kein Prüfungsergebnis
BtnAuthLDAP: Auf Campus-Kennung umstellen
BtnAuthPWHash: Auf Uni2work-Kennung umstellen
@ -1363,8 +1414,8 @@ BtnPasswordReset: Passwort zurücksetzen
AuthLDAPLookupFailed: Nutzer konnte aufgrund eines LDAP-Fehlers nicht nachgeschlagen werden
AuthLDAPInvalidLookup: Bestehender Nutzer konnte nicht eindeutig einem LDAP-Eintrag zugeordnet werden
AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung an
AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung an
AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung in Uni2work an
AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung in Uni2work an
AuthPWHashAlreadyConfigured: Nutzer meldet sich bereits per Uni2work-Kennung an
AuthPWHashConfigured: Nutzer meldet sich nun per Uni2work-Kennung an

View File

@ -31,38 +31,14 @@ AllocationCourse
allocation AllocationId
course CourseId
minCapacity Int -- if the course would get assigned fewer than this many applicants, restart the assignment process without the course
instructions Html Maybe -- instructions from the lecturer to applicants
applicationText Bool -- lecturer will read application texts supplied by users
applicationFiles UploadMode -- lecturer wants to receive course specific application files
ratingsVisible Bool -- lecturer wants applicants to receive feedback on their application (Grade & comment)
UniqueAllocationCourse course
AllocationCourseFile
allocationCourse AllocationCourseId
file FileId
UniqueAllocationCourseFile allocationCourse file
AllocationUser
allocation AllocationId
user UserId
totalCourses Natural -- number of total allocated courses for this user must be <= than this number
UniqueAllocationUser allocation user
AllocationApplication
allocationCourse AllocationCourseId
allocationUser AllocationUserId
text Text Maybe -- free text entered by user
priority Natural -- priority, higher number means higher priority
ratingVeto Bool
ratingPoints ExamGrade Maybe
ratingComment Text Maybe
UniqueAllocationApplication allocationCourse allocationUser
AllocationApplicationFile -- supplemental file for application by a user for a certain course
application AllocationApplicationId
file FileId
UniqueAllocationUserFile application file
AllocationDeregister -- self-inflicted user-deregistrations from an allocated course
user UserId
allocation AllocationId Maybe

View File

@ -17,9 +17,20 @@ Course -- Information about a single course; contained info is always visible
deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase
materialFree Bool -- False: only enrolled users may see course materials not stored in this table
applicationsRequired Bool
applicationsInstructions Html Maybe
applicationsText Bool
applicationsFiles UploadMode
applicationsRatingsVisible Bool
TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic
CourseAppInstructionFile
course CourseId
file FileId
UniqueCourseAppInstructionFile course file
CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables)
user UserId
time UTCTime
@ -59,3 +70,18 @@ CourseUserNoteEdit -- who edited a participants course note when
user UserId
time UTCTime
note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more
CourseApplication
course CourseId
user UserId
field StudyFeaturesId Maybe -- associated degree course, user-defined; required for communicating grades
text Text Maybe -- free text entered by user
ratingPoints ExamGrade Maybe
ratingComment Text Maybe
allocation AllocationId Maybe
allocationPriority Natural Maybe
time UTCTime default=now()
CourseApplicationFile
application CourseApplicationId
file FileId
UniqueApplicationFile application file

15677
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "uni2work",
"version": "4.13.1",
"version": "5.0.2",
"description": "",
"keywords": [],
"author": "",

View File

@ -1,5 +1,5 @@
name: uniworx
version: 4.13.1
version: 5.0.2
dependencies:
# Due to a bug in GHC 8.0.1, we block its usage

7
routes
View File

@ -86,7 +86,8 @@
!/course/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !free
/register CRegisterR GET POST !timeANDcapacityANDallocation-time !lecturerANDallocation-time
/register CRegisterR GET POST !timeANDcapacityANDallocation-timeAND¬exam-result !lecturerANDallocation-time
/register-template CRegisterTemplateR GET !free
/edit CEditR GET POST
/lecturer-invite CLecInviteR GET POST
/delete CDeleteR GET POST !lecturerANDemptyANDallocation-time
@ -152,7 +153,9 @@
/users EUsersR GET POST
/users/new EAddUserR GET POST
/users/invite EInviteR GET POST
/register ERegisterR POST !timeANDcourse-registered !timeANDexam-registered
/register ERegisterR POST !timeANDcourse-registeredAND¬exam-registered !timeANDexam-registeredAND¬exam-result
/apps/#CryptoFileNameCourseApplication CourseApplicationR:
/files CAFilesR GET !self !lecturerANDtime
/subs CorrectionsR GET POST !corrector !lecturer
/subs/upload CorrectionsUploadR GET POST !corrector !lecturer

View File

@ -30,6 +30,24 @@ data Transaction
{ transactionExam :: ExamId
, transactionUser :: UserId
}
| TransactionCourseParticipantEdit
{ transactionCourse :: CourseId
, transactionUser :: UserId
}
| TransactionCourseParticipantDeleted
{ transactionCourse :: CourseId
, transactionUser :: UserId
}
| TransactionCourseApplicationEdit
{ transactionCourse :: CourseId
, transactionUser :: UserId
, transactionCourseApplication :: CourseApplicationId
}
| TransactionCourseApplicationDeleted
{ transactionCourse :: CourseId
, transactionUser :: UserId
, transactionCourseApplication :: CourseApplicationId
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions

View File

@ -29,6 +29,8 @@ import qualified Data.CaseInsensitive as CI
import Data.Aeson (ToJSON(..), ToJSONKey(..), ToJSONKeyFunction(..), FromJSON(..), FromJSONKey(..), FromJSONKeyFunction(..), Value(..), withText)
import Data.Aeson.Encoding (text)
import Text.Blaze (ToMarkup(..))
instance {-# OVERLAPPING #-} MonadThrow m => MonadCrypto (ReaderT CryptoIDKey m) where
type MonadCryptoKey (ReaderT CryptoIDKey m) = CryptoIDKey
@ -46,19 +48,23 @@ decCryptoIDs [ ''SubmissionId
, ''ExamOccurrenceId
, ''ExamPartId
, ''AllocationId
, ''CourseApplicationId
]
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => PathPiece (E.CryptoID namespace (CI FilePath)) where
-- CryptoIDNamespace (CI FilePath) SubmissionId ~ "Submission"
instance {-# OVERLAPS #-} PathPiece (E.CryptoID "Submission" (CI FilePath)) where
fromPathPiece (Text.unpack -> piece) = do
piece' <- (stripPrefix `on` map CI.mk) "uwa" piece
return . CryptoID . CI.mk $ map CI.original piece'
toPathPiece = Text.pack . ("uwa" <>) . CI.foldedCase . ciphertext
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => ToJSON (E.CryptoID namespace (CI FilePath)) where
instance {-# OVERLAPS #-} ToJSON (E.CryptoID "Submission" (CI FilePath)) where
toJSON = String . toPathPiece
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => ToJSONKey (E.CryptoID namespace (CI FilePath)) where
instance {-# OVERLAPS #-} ToJSONKey (E.CryptoID "Submission" (CI FilePath)) where
toJSONKey = ToJSONKeyText toPathPiece (text . toPathPiece)
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => FromJSON (E.CryptoID namespace (CI FilePath)) where
instance {-# OVERLAPS #-} FromJSON (E.CryptoID "Submission" (CI FilePath)) where
parseJSON = withText "CryptoFileNameSubmission" $ maybe (fail "Could not parse CryptoFileNameSubmission") return . fromPathPiece
instance {-# OVERLAPS #-} namespace ~ CryptoIDNamespace (CI FilePath) SubmissionId => FromJSONKey (E.CryptoID namespace (CI FilePath)) where
instance {-# OVERLAPS #-} FromJSONKey (E.CryptoID "Submission" (CI FilePath)) where
fromJSONKey = FromJSONKeyTextParser $ maybe (fail "Could not parse CryptoFileNameSubmission") return . fromPathPiece
instance {-# OVERLAPS #-} ToMarkup (E.CryptoID "Submission" (CI FilePath)) where
toMarkup = toMarkup . toPathPiece

View File

@ -10,5 +10,11 @@ import Text.Blaze (ToMarkup(..))
import ClassyPrelude
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
instance {-# OVERLAPS #-} ToMarkup s => ToMarkup (CID.CryptoID c (CI s)) where
toMarkup = toMarkup . CI.foldedCase . CID.ciphertext
instance ToMarkup s => ToMarkup (CID.CryptoID c s) where
toMarkup = toMarkup . CID.ciphertext
toMarkup = toMarkup . CID.ciphertext

View File

@ -54,6 +54,7 @@ import Data.Conduit (($$))
import Data.Conduit.List (sourceList)
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Control.Monad.Except (MonadError(..), ExceptT)
import Control.Monad.Trans.Maybe (MaybeT(..))
@ -150,6 +151,7 @@ deriving instance Generic SubmissionR
deriving instance Generic MaterialR
deriving instance Generic TutorialR
deriving instance Generic ExamR
deriving instance Generic CourseApplicationR
deriving instance Generic (Route UniWorX)
-- | Convenient Type Synonyms:
@ -179,6 +181,10 @@ pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Cr
pattern CSubmissionR tid ssh csh shn cid ptn
= CSheetR tid ssh csh shn (SubmissionR cid ptn)
pattern CApplicationR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> CourseApplicationR -> Route UniWorX
pattern CApplicationR tid ssh csh appId ptn
= CourseR tid ssh csh (CourseApplicationR appId ptn)
pluralDE :: (Eq a, Num a)
=> a -- ^ Count
@ -282,6 +288,7 @@ embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id
embedRenderMessage ''UniWorX ''SubmissionFileType id
embedRenderMessage ''UniWorX ''CorrectorState id
embedRenderMessage ''UniWorX ''RatingException id
embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>)
@ -518,6 +525,11 @@ andAR _ _ reason@(Unauthorized _) = reason
andAR _ Authorized other = other
andAR _ AuthenticationRequired _ = AuthenticationRequired
notAR :: RenderMessage UniWorX msg => MsgRenderer -> msg -> AuthResult -> AuthResult
notAR _ _ (Unauthorized _) = Authorized
notAR _ _ AuthenticationRequired = AuthenticationRequired
notAR mr msg Authorized = Unauthorized . render mr . MsgUnauthorizedNot $ render mr msg
trueAR, falseAR :: MsgRendererS UniWorX -> AuthResult
trueAR = const Authorized
falseAR = Unauthorized . ($ MsgUnauthorized) . render
@ -580,14 +592,13 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of
-- Courses: access only to school admins
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` userAdmin) -> do
isAdmin <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` userAdmin) -> do
E.on $ course E.^. CourseSchool E.==. userAdmin E.^. UserAdminSchool
E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedSchoolAdmin)
guardMExceptT isAdmin (unauthorizedI MsgUnauthorizedSchoolAdmin)
return Authorized
-- other routes: access to any admin is granted here
_other -> $cachedHereBinary mAuthId . exceptT return return $ do
@ -622,14 +633,13 @@ tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- lift . E.select . E.from $ \(course `E.InnerJoin` lecturer) -> do
isLecturer <- lift . E.selectExists . E.from $ \(course `E.InnerJoin` lecturer) -> do
E.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
guardMExceptT isLecturer (unauthorizedI MsgUnauthorizedLecturer)
return Authorized
-- lecturer for any school will do
_ -> $cachedHereBinary mAuthId . exceptT return return $ do
@ -688,6 +698,22 @@ tagAccessPredicate AuthTutor = APDB $ \mAuthId route _ -> exceptT return return
guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
return Authorized
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
CApplicationR tid ssh csh _ _ -> maybeT (unauthorizedI MsgUnauthorizedApplicationTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
allocationCourse <- $cachedHereBinary course . lift . getBy $ UniqueAllocationCourse course
allocation <- for allocationCourse $ \(Entity _ AllocationCourse{..}) -> $cachedHereBinary allocationCourseAllocation . MaybeT $ get allocationCourseAllocation
case allocation of
Nothing -> return ()
Just Allocation{..} -> do
cTime <- liftIO getCurrentTime
guard $ NTop allocationStaffAllocationFrom <= NTop (Just cTime)
guard $ NTop (Just cTime) <= NTop allocationStaffAllocationTo
return Authorized
CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn
@ -781,9 +807,20 @@ tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of
| not registered
, maybe False (now >=) courseRegisterFrom -- Nothing => no registration allowed
, maybe True (now <=) courseRegisterTo -> return Authorized
(Just (Entity _ Course{courseDeregisterUntil}))
(Just (Entity cid Course{courseDeregisterUntil}))
| registered
, maybe True (now <=) courseDeregisterUntil -> return Authorized
-> maybeT (unauthorizedI MsgUnauthorizedCourseTime) $ do
guard $ maybe True (now <=) courseDeregisterUntil
forM_ mAuthId $ \uid -> do
exams <- lift . E.select . E.from $ \exam -> do
E.where_ . E.exists . E.from $ \examRegistration ->
E.where_ $ examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
return $ exam E.^. ExamDeregisterUntil
forM_ exams $ \(E.Value deregUntil) ->
guard $ NTop (Just now) >= NTop deregUntil
return Authorized
_other -> unauthorizedI MsgUnauthorizedCourseTime
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do
@ -844,20 +881,19 @@ tagAccessPredicate AuthAllocationTime = APDB $ \mAuthId route _ -> case route of
tagAccessPredicate AuthCourseRegistered = APDB $ \mAuthId route _ -> case route of
CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.select . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` courseParticipant) -> do
E.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
r -> $unsupportedAuthPredicate AuthCourseRegistered r
tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case route of
CTutorialR tid ssh csh tutn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh, tutn) . lift . E.select . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
isRegistered <- $cachedHereBinary (authId, tid, ssh, csh, tutn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId
@ -865,26 +901,24 @@ tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case rout
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. tutorial E.^. TutorialName E.==. E.val tutn
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.select . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
isRegistered <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` tutorial `E.InnerJoin` tutorialParticipant) -> do
E.on $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
r -> $unsupportedAuthPredicate AuthTutorialRegistered r
tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.select . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
@ -892,20 +926,47 @@ tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
[E.Value c] <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.select . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
hasRegistration <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` examRegistration) -> do
E.on $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64))
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
return Authorized
r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthExamResult = APDB $ \mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasResult <- $cachedHereBinary (authId, tid, ssh, csh, examn) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` (examResult `E.FullOuterJoin` (examPartResult `E.InnerJoin` examPart))) -> do
E.on $ examPartResult E.?. ExamPartResultExamPart E.==. examPart E.?. ExamPartId
E.on $ examResult E.?. ExamResultExam E.==. examPart E.?. ExamPartExam
E.on $ E.just (exam E.^. ExamId) E.==. examResult E.?. ExamResultExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ (examResult E.?. ExamResultUser E.==. E.just (E.val authId) E.||. examPartResult E.?. ExamPartResultUser E.==. E.just (E.val authId))
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn
guardMExceptT hasResult (unauthorizedI MsgUnauthorizedExamResult)
return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId
hasResult <- $cachedHereBinary (authId, tid, ssh, csh) . lift . E.selectExists . E.from $ \(course `E.InnerJoin` exam `E.InnerJoin` (examResult `E.FullOuterJoin` (examPartResult `E.InnerJoin` examPart))) -> do
E.on $ examPartResult E.?. ExamPartResultExamPart E.==. examPart E.?. ExamPartId
E.on $ examResult E.?. ExamResultExam E.==. examPart E.?. ExamPartExam
E.on $ E.just (exam E.^. ExamId) E.==. examResult E.?. ExamResultExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ (examResult E.?. ExamResultUser E.==. E.just (E.val authId) E.||. examPartResult E.?. ExamPartResultUser E.==. E.just (E.val authId))
E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
guardMExceptT hasResult (unauthorizedI MsgUnauthorizedExamResult)
return Authorized
r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthParticipant = APDB $ \_ route _ -> case route of
@ -1004,10 +1065,9 @@ tagAccessPredicate AuthEmpty = APDB $ \_ route _ -> case route of
-- Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
assertM_ (<= 0) . $cachedHereBinary cid . lift $ count [ CourseParticipantCourse ==. cid ]
assertM_ ((<= 0) :: Int -> Bool) . $cachedHereBinary cid . lift . fmap (E.unValue . unsafeHead) $ E.select . E.from $ \(sheet `E.InnerJoin` submission) -> do
assertM_ not . $cachedHereBinary cid . lift $ E.selectExists . E.from $ \(sheet `E.InnerJoin` submission) -> do
E.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
return E.countRows
return Authorized
r -> $unsupportedAuthPredicate AuthEmpty r
tagAccessPredicate AuthMaterials = APDB $ \_ route _ -> case route of
@ -1044,19 +1104,24 @@ tagAccessPredicate AuthCorrectorSubmissions = APDB $ \_ route _ -> case route of
guard submissionModeCorrector
return Authorized
r -> $unsupportedAuthPredicate AuthCorrectorSubmissions r
tagAccessPredicate AuthSelf = APHandler $ \mAuthId route _ -> exceptT return return $ do
tagAccessPredicate AuthSelf = APDB $ \mAuthId route _ -> exceptT return return $ do
referencedUser <- case route of
AdminUserR cID -> return cID
AdminUserDeleteR cID -> return cID
AdminHijackUserR cID -> return cID
UserNotificationR cID -> return cID
UserPasswordR cID -> return cID
CourseR _ _ _ (CUserR cID) -> return cID
AdminUserR cID -> decrypt cID
AdminUserDeleteR cID -> decrypt cID
AdminHijackUserR cID -> decrypt cID
UserNotificationR cID -> decrypt cID
UserPasswordR cID -> decrypt cID
CourseR _ _ _ (CUserR cID) -> decrypt cID
CApplicationR _ _ _ cID _ -> do
appId <- decrypt cID
application <- $cachedHereBinary appId . lift $ get appId
case application of
Nothing -> throwError =<< unauthorizedI MsgUnauthorizedSelf
Just CourseApplication{..} -> return courseApplicationUser
_other -> throwError =<< $unsupportedAuthPredicate AuthSelf route
referencedUser' <- decrypt referencedUser
case mAuthId of
Just uid
| uid == referencedUser' -> return Authorized
| uid == referencedUser -> return Authorized
Nothing -> return AuthenticationRequired
_other -> unauthorizedI MsgUnauthorizedSelf
tagAccessPredicate AuthIsLDAP = APDB $ \_ route _ -> exceptT return return $ do
@ -1159,10 +1224,7 @@ evalAuthTags AuthTagActive{..} (map (Set.toList . toNullable) . Set.toList . dnf
evalAuthLiteral :: AuthLiteral -> WriterT (Set AuthTag) m AuthResult
evalAuthLiteral PLVariable{..} = evalAuthTag plVar
evalAuthLiteral PLNegated{..} = evalAuthTag plVar >>= \case
Unauthorized _ -> return Authorized
AuthenticationRequired -> return AuthenticationRequired
Authorized -> unauthorizedI plVar
evalAuthLiteral PLNegated{..} = notAR mr plVar <$> evalAuthTag plVar
orAR', andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult
orAR' = shortCircuitM (is _Authorized) (orAR mr)
@ -1616,13 +1678,13 @@ instance YesodBreadcrumbs UniWorX where
breadcrumb (CourseR tid ssh csh CTutorialListR) = return ("Tutorien", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CTutorialNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CTutorialListR)
breadcrumb (CourseR tid ssh csh CExamListR) = return ("Klausuren", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CExamListR) = return ("Prüfungen", Just $ CourseR tid ssh csh CShowR)
breadcrumb (CourseR tid ssh csh CExamNewR) = return ("Anlegen", Just $ CourseR tid ssh csh CExamListR)
breadcrumb (CExamR tid ssh csh examn EShowR) = return (original examn, Just $ CourseR tid ssh csh CExamListR)
breadcrumb (CExamR tid ssh csh examn EEditR) = return ("Bearbeiten", Just $ CExamR tid ssh csh examn EShowR)
breadcrumb (CExamR tid ssh csh examn EUsersR) = return ("Teilnehmer", Just $ CExamR tid ssh csh examn EShowR)
breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Klausurteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR)
breadcrumb (CExamR tid ssh csh examn EAddUserR) = return ("Prüfungsteilnehmer hinzufügen", Just $ CExamR tid ssh csh examn EUsersR)
breadcrumb (CTutorialR tid ssh csh tutn TUsersR) = return (original tutn, Just $ CourseR tid ssh csh CTutorialListR)
breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR)
@ -2731,6 +2793,7 @@ routeNormalizers =
, ncCourse
, ncSheet
, verifySubmission
, verifyCourseApplication
]
where
normalizeRender route = route <$ do
@ -2777,6 +2840,14 @@ routeNormalizers =
let newRoute = CSubmissionR courseTerm courseSchool courseShorthand sheetName cID sr
tell . Any $ route /= newRoute
return newRoute
verifyCourseApplication = maybeOrig $ \route -> do
CApplicationR _tid _ssh _csh cID sr <- return route
aId <- decrypt cID
CourseApplication{courseApplicationCourse} <- lift . lift $ get404 aId
Course{courseTerm, courseSchool, courseShorthand} <- lift . lift $ get404 courseApplicationCourse
let newRoute = CApplicationR courseTerm courseSchool courseShorthand cID sr
tell . Any $ route /= newRoute
return newRoute
-- How to run database actions.

View File

@ -15,6 +15,7 @@ import Handler.Course.Register as Handler.Course
import Handler.Course.Show as Handler.Course
import Handler.Course.User as Handler.Course
import Handler.Course.Users as Handler.Course
import Handler.Course.Application as Handler.Course
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html

View File

@ -0,0 +1,37 @@
module Handler.Course.Application
( getCAFilesR
) where
import Import
import Handler.Utils
import qualified Database.Esqueleto as E
import System.FilePath (addExtension)
import qualified Data.Conduit.List as C
getCAFilesR :: TermId -> SchoolId -> CourseShorthand -> CryptoFileNameCourseApplication -> Handler TypedContent
getCAFilesR tid ssh csh cID = do
appId <- decrypt cID
User{..} <- runDB $ do
CourseApplication{..} <- get404 appId
Course{..} <- get404 courseApplicationCourse
let matches = and
[ tid == courseTerm
, ssh == courseSchool
, csh == courseShorthand
]
unless matches . redirectWith movedPermanently301 $ CApplicationR courseTerm courseSchool courseShorthand cID CAFilesR
get404 courseApplicationUser
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgCourseApplicationArchiveName tid ssh csh cID userDisplayName
let
fsSource = E.selectSource . E.from $ \(courseApplicationFile `E.InnerJoin` file) -> do
E.on $ courseApplicationFile E.^. CourseApplicationFileFile E.==. file E.^. FileId
E.where_ $ courseApplicationFile E.^. CourseApplicationFileApplication E.==. E.val appId
return file
serveSomeFiles archiveName $ fsSource .| C.map entityVal

View File

@ -41,6 +41,12 @@ data CourseForm = CourseForm
, cfLink :: Maybe Text
, cfMatFree :: Bool
, cfAllocation :: Maybe AllocationCourseForm
, cfAppRequired :: Bool
, cfAppInstructions :: Maybe Html
, cfAppInstructionFiles :: Maybe (Source Handler (Either FileId File))
, cfAppText :: Bool
, cfAppFiles :: UploadMode
, cfAppRatingsVisible :: Bool
, cfCapacity :: Maybe Int
, cfSecret :: Maybe Text
, cfRegFrom :: Maybe UTCTime
@ -51,43 +57,45 @@ data CourseForm = CourseForm
data AllocationCourseForm = AllocationCourseForm
{ acfAllocation :: AllocationId
, acfInstructions :: Maybe Html
, acfFiles :: Maybe (Source Handler (Either FileId File))
, acfApplicationText :: Bool
, acfApplicationFiles :: UploadMode
, acfApplicationRatingsVisible :: Bool
, acfMinCapacity :: Int
}
courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid
, cfName = courseName
, cfDesc = courseDescription
, cfLink = courseLinkExternal
, cfShort = courseShorthand
, cfTerm = courseTerm
, cfSchool = courseSchool
, cfCapacity = courseCapacity
, cfSecret = courseRegisterSecret
, cfMatFree = courseMaterialFree
, cfRegFrom = courseRegisterFrom
, cfRegTo = courseRegisterTo
, cfDeRegUntil = courseDeregisterUntil
, cfAllocation = allocationCourseToForm <$> alloc
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ]
{ cfCourseId = Just cid
, cfName = courseName
, cfDesc = courseDescription
, cfLink = courseLinkExternal
, cfShort = courseShorthand
, cfTerm = courseTerm
, cfSchool = courseSchool
, cfCapacity = courseCapacity
, cfSecret = courseRegisterSecret
, cfMatFree = courseMaterialFree
, cfAllocation = allocationCourseToForm <$> alloc
, cfAppRequired = courseApplicationsRequired
, cfAppInstructions = courseApplicationsInstructions
, cfAppInstructionFiles
, cfAppText = courseApplicationsText
, cfAppFiles = courseApplicationsFiles
, cfAppRatingsVisible = courseApplicationsRatingsVisible
, cfRegFrom = courseRegisterFrom
, cfRegTo = courseRegisterTo
, cfDeRegUntil = courseDeregisterUntil
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs]
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ]
}
where
cfAppInstructionFiles = Just . transPipe runDB $ selectAppFiles .| C.map (Left . E.unValue)
where selectAppFiles = E.selectSource . E.from $ \courseAppInstructionFile -> do
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid
return $ courseAppInstructionFile E.^. CourseAppInstructionFileFile
allocationCourseToForm :: Entity AllocationCourse -> AllocationCourseForm
allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm
{ acfAllocation = allocationCourseAllocation
, acfMinCapacity = allocationCourseMinCapacity
, acfInstructions = allocationCourseInstructions
, acfFiles = Nothing
, acfApplicationText = allocationCourseApplicationText
, acfApplicationFiles = allocationCourseApplicationFiles
, acfApplicationRatingsVisible = allocationCourseRatingsVisible
}
makeCourseForm :: (forall p. PathPiece p => p -> Maybe (SomeRoute UniWorX)) -> Maybe CourseForm -> Form CourseForm
@ -213,21 +221,9 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse $ \html -> do
_ -> do
allocationOptions <- mkOptionList <$> mapM mkAllocationOption availableAllocations
oldFileIds <- for ((,) <$> (fmap acfAllocation $ template >>= cfAllocation) <*> (template >>= cfCourseId)) $ \(allId, cId) -> fmap (Set.fromList . map E.unValue) . liftHandlerT . runDB . E.select . E.from $ \(allocationCourseFile `E.InnerJoin` allocationCourse) -> do
E.on $ allocationCourseFile E.^. AllocationCourseFileAllocationCourse E.==. allocationCourse E.^. AllocationCourseId
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cId
E.&&. allocationCourse E.^. AllocationCourseAllocation E.==. E.val allId
return $ allocationCourseFile E.^. AllocationCourseFileFile
let
allocationForm' = AllocationCourseForm
<$> apreq (selectField' Nothing $ return allocationOptions) (fslI MsgCourseAllocation) (fmap acfAllocation $ template >>= cfAllocation)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslI MsgCourseAllocationInstructions & setTooltip MsgCourseAllocationInstructionsTip) (fmap acfInstructions $ template >>= cfAllocation))
<*> aopt (multiFileField . return $ fromMaybe Set.empty oldFileIds) (fslI MsgCourseAllocationApplicationTemplate) (fmap acfFiles $ template >>= cfAllocation)
<*> apopt checkBoxField (fslI MsgCourseAllocationApplicationText & setTooltip MsgCourseAllocationApplicationTextTip) (fmap acfApplicationText $ template >>= cfAllocation)
<*> uploadModeForm (fmap acfApplicationFiles $ template >>= cfAllocation)
<*> apopt checkBoxField (fslI MsgCourseAllocationApplicationRatingsVisible & setTooltip MsgCourseAllocationApplicationRatingsVisibleTip) (fmap acfApplicationRatingsVisible $ template >>= cfAllocation)
<*> apreq (natFieldI MsgCourseAllocationMinCapacityMustBeNonNegative) (fslI MsgCourseAllocationMinCapacity & setTooltip MsgCourseAllocationMinCapacityTip) (fmap acfMinCapacity $ template >>= cfAllocation)
optionalActionW allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template)
@ -247,6 +243,12 @@ makeCourseForm miButtonAction template = identifyForm FIDcourse $ \html -> do
<*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<* aformSection MsgCourseFormSectionRegistration
<*> allocationForm
<*> apopt checkBoxField (fslI MsgCourseApplicationRequired & setTooltip MsgCourseApplicationRequiredTip) (cfAppRequired <$> template)
<*> (assertM (not . null . renderHtml) <$> aopt htmlField (fslI MsgCourseApplicationInstructions & setTooltip MsgCourseApplicationInstructionsTip) (cfAppInstructions <$> template))
<*> aopt (multiFileField' . fromMaybe (return ()) $ cfAppInstructionFiles =<< template) (fslI MsgCourseApplicationTemplate & setTooltip MsgCourseApplicationTemplateTip) (cfAppInstructionFiles <$> template)
<*> apopt checkBoxField (fslI MsgCourseApplicationsText & setTooltip MsgCourseApplicationsTextTip) (cfAppText <$> template)
<*> uploadModeForm (cfAppFiles <$> template)
<*> apopt checkBoxField (fslI MsgCourseApplicationRatingsVisible & setTooltip MsgCourseApplicationRatingsVisibleTip) (cfAppRatingsVisible <$> template)
<*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
<*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat)
@ -425,20 +427,26 @@ courseEditHandler miButtonAction mbCourseForm = do
} -> do -- create new course
now <- liftIO getCurrentTime
insertOkay <- runDBJobs $ do
insertOkay <- insertUnique Course
{ courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
, courseShorthand = cfShort res
, courseTerm = cfTerm res
, courseSchool = cfSchool res
, courseCapacity = cfCapacity res
, courseRegisterSecret = cfSecret res
, courseMaterialFree = cfMatFree res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
, courseDeregisterUntil = cfDeRegUntil res
}
insertOkay <- let CourseForm{..} = res
in insertUnique Course
{ courseName = cfName
, courseDescription = cfDesc
, courseLinkExternal = cfLink
, courseShorthand = cfShort
, courseTerm = cfTerm
, courseSchool = cfSchool
, courseCapacity = cfCapacity
, courseRegisterSecret = cfSecret
, courseMaterialFree = cfMatFree
, courseApplicationsRequired = cfAppRequired
, courseApplicationsInstructions = cfAppInstructions
, courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
whenIsJust insertOkay $ \cid -> do
let (invites, adds) = partitionEithers $ cfLecturers res
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
@ -466,20 +474,26 @@ courseEditHandler miButtonAction mbCourseForm = do
case old of
Nothing -> addMessageI Error MsgInvalidInput $> False
(Just _) -> do
updOkay <- myReplaceUnique cid Course
{ courseName = cfName res
, courseDescription = cfDesc res
, courseLinkExternal = cfLink res
, courseShorthand = cfShort res
, courseTerm = cfTerm res -- dangerous
, courseSchool = cfSchool res
, courseCapacity = cfCapacity res
, courseRegisterSecret = cfSecret res
, courseMaterialFree = cfMatFree res
, courseRegisterFrom = cfRegFrom res
, courseRegisterTo = cfRegTo res
, courseDeregisterUntil = cfDeRegUntil res
}
updOkay <- let CourseForm{..} = res
in myReplaceUnique cid Course
{ courseName = cfName
, courseDescription = cfDesc
, courseLinkExternal = cfLink
, courseShorthand = cfShort
, courseTerm = cfTerm -- dangerous
, courseSchool = cfSchool
, courseCapacity = cfCapacity
, courseRegisterSecret = cfSecret
, courseMaterialFree = cfMatFree
, courseApplicationsRequired = cfAppRequired
, courseApplicationsInstructions = cfAppInstructions
, courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
case updOkay of
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
Nothing -> do
@ -490,7 +504,19 @@ courseEditHandler miButtonAction mbCourseForm = do
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
insert_ $ CourseEdit aid now cid
let
finsert val = do
fId <- lift $ either return insert val
tell $ Set.singleton fId
lift $
void . insertUnique $ CourseAppInstructionFile cid fId
keep <- execWriterT . runConduit $ transPipe liftHandlerT (traverse_ id $ cfAppInstructionFiles res) .| C.mapM_ finsert
acfs <- selectList [ CourseAppInstructionFileCourse ==. cid, CourseAppInstructionFileFile /<-. Set.toList keep ] []
mapM_ deleteCascade $ map (courseAppInstructionFileFile . entityVal) acfs
upsertAllocationCourse cid $ cfAllocation res
addMessageI Success $ MsgCourseEditOk tid ssh csh
return True
when success $ redirect $ CourseR tid ssh csh CShowR
@ -522,38 +548,17 @@ upsertAllocationCourse cid cfAllocation = do
when doEdit $
case cfAllocation of
Just AllocationCourseForm{..} -> do
Entity acId _ <- upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
, allocationCourseInstructions = acfInstructions
, allocationCourseApplicationText = acfApplicationText
, allocationCourseApplicationFiles = acfApplicationFiles
, allocationCourseRatingsVisible = acfApplicationRatingsVisible
Just AllocationCourseForm{..} ->
void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity
}
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseInstructions =. acfInstructions
, AllocationCourseApplicationText =. acfApplicationText
, AllocationCourseApplicationFiles =. acfApplicationFiles
, AllocationCourseRatingsVisible =. acfApplicationRatingsVisible
[ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity
]
let
finsert val = do
fId <- lift $ either return insert val
tell $ Set.singleton fId
lift $
void . insertUnique $ AllocationCourseFile acId fId
keep <- execWriterT . runConduit $ transPipe liftHandlerT (traverse_ id acfFiles) .| C.mapM_ finsert
acfs <- selectList [ AllocationCourseFileAllocationCourse ==. acId, AllocationCourseFileFile /<-. Set.toList keep ] []
mapM_ deleteCascade $ map (allocationCourseFileFile . entityVal) acfs
Nothing
| Just (Entity prevId _) <- prevAllocationCourse
-> do
acfs <- selectList [ AllocationCourseFileAllocationCourse ==. prevId ] []
mapM_ deleteCascade $ map (allocationCourseFileFile . entityVal) acfs
delete prevId
-> delete prevId
_other -> return ()

View File

@ -8,6 +8,8 @@ module Handler.Course.List
import Import
import Data.Maybe (fromJust)
import Utils.Lens
import Utils.Form
-- import Utils.DB
@ -20,83 +22,43 @@ import qualified Data.Set as Set
import qualified Data.Map as Map
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
-- NOTE: Outdated way to use dbTable; see ProfileDataR Handler for a more recent method.
type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School)
type CourseTableData = DBRow (Entity Course, Int, Bool, Entity School, [Entity User])
colCourse :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR)
[whamlet|_{courseName}|]
-- colCourseDescr :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
-- colCourseDescr = sortable (Just "course") (i18nCell MsgCourse) $ do
-- course <- view $ _dbrOutput . _1 . _entityVal
-- return $ courseCell course
colDescription :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colDescription = sortable Nothing mempty
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
case courseDescription of
Nothing -> mempty
(Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr)
colCShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|_{courseShorthand}|]
-- colCShortDescr :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
-- colCShortDescr = sortable (Just "cshort") (i18nCell MsgCourseShort)
-- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> mappend
-- ( anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|#{display courseShorthand}|] )
-- ( case courseDescription of
-- Nothing -> mempty
-- (Just descr) -> cell
-- [whamlet|
-- $newline never
-- <div>
-- ^{modal "Beschreibung" (Right $ toWidget descr)}
-- |]
-- )
colTerm :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (TermCourseListR courseTerm) [whamlet|#{courseTerm}|]
-- colSchool :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
-- colSchool = sortable (Just "school") (i18nCell MsgCourseSchool)
-- $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
-- anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|_{schoolName}|]
colSchoolShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } ->
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}, _) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|_{schoolShorthand}|]
colRegFrom :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegFrom = sortable (Just "register-from") (i18nCell MsgRegisterFrom)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterFrom
-- cell $ traverse (formatTime SelFormatDateTime) courseRegisterFrom >>= maybe mempty toWidget
colRegTo :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegTo = sortable (Just "register-to") (i18nCell MsgRegisterTo)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } ->
maybe mempty dateTimeCell courseRegisterTo
colMembers :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colMembers = sortable (Just "members") (i18nCell MsgCourseMembers)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, currentParticipants, _, _) } -> i18nCell $ case courseCapacity of
Nothing -> MsgCourseMembersCount currentParticipants
Just limit -> MsgCourseMembersCountLimited currentParticipants limit
colRegistered :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegistered = sortable (Just "registered") (i18nCell MsgRegistered)
$ \DBRow{ dbrOutput=(_, _, registered, _) } -> tickmarkCell registered
$ \DBRow{ dbrOutput=(_, _, registered, _, _) } -> tickmarkCell registered
type CourseTableExpr = E.SqlExpr (Entity Course) `E.InnerJoin` E.SqlExpr (Entity School)
@ -114,6 +76,7 @@ makeCourseTable :: ( IsDBTable m x, ToSortable h, Functor h, DBResult m x ~ ((),
=> _ -> Colonnade h CourseTableData (DBCell m x) -> PSValidator m x -> DB Widget
makeCourseTable whereClause colChoices psValidator = do
muid <- lift maybeAuthId
now <- liftIO getCurrentTime
let dbtSQLQuery :: CourseTableExpr -> E.SqlQuery _
dbtSQLQuery qin@(course `E.InnerJoin` school) = do
E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId
@ -121,8 +84,14 @@ makeCourseTable whereClause colChoices psValidator = do
let registered = course2Registered muid qin
E.where_ $ whereClause (course, participants, registered)
return (course, participants, registered, school)
lecturerQuery cid (user `E.InnerJoin` lecturer) = do
E.on $ user E.^. UserId E.==. lecturer E.^. LecturerUser
E.where_ $ cid E.==. lecturer E.^. LecturerCourse E.&&. lecturer E.^. LecturerType E.==. E.val CourseLecturer
return user
dbtProj :: DBRow _ -> MaybeT (ReaderT SqlBackend (HandlerT UniWorX IO)) CourseTableData
dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> return (course, participants, registered, school)
dbtProj = traverse $ \(course, E.Value participants, E.Value registered, school) -> do
lecturerList <- lift $ E.select $ E.from $ lecturerQuery $ E.val $ entityKey course
return (course, participants, registered, school, lecturerList)
snd <$> dbTable psValidator DBTable
{ dbtSQLQuery
, dbtRowKey = \(course `E.InnerJoin` _) -> course E.^. CourseId
@ -163,6 +132,18 @@ makeCourseTable whereClause colChoices psValidator = do
| Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> school E.^. SchoolShorthand `E.in_` E.valList (Set.toList criterias)
)
, ( "lecturer", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterias -> if
| Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> E.exists $ E.from $ \t -> do
user <- lecturerQuery (course E.^. CourseId) t
E.where_ $ E.any (E.hasInfix (user E.^. UserSurname) . E.val) (criterias :: Set.Set Text)
)
, ( "openregistration", FilterColumn $ \(course `E.InnerJoin` _school :: CourseTableExpr) criterion -> case getLast (criterion :: Last Bool) of
Nothing -> E.val True
Just b -> let regTo = course E.^. CourseRegisterTo
regFrom = course E.^. CourseRegisterFrom
in (E.==.) (E.val b) $ (E.isNothing regTo E.||. E.val (Just now) E.<=. regTo) E.&&. E.val (Just now) E.>=. regFrom
)
, ( "registered", FilterColumn $ \tExpr criterion -> case getLast (criterion :: Last Bool) of
Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
Just needle -> course2Registered muid tExpr E.==. E.val needle
@ -175,10 +156,18 @@ makeCourseTable whereClause colChoices psValidator = do
)
]
, dbtFilterUI = \mPrev -> mconcat $ catMaybes
[ Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch)
[ Just $ prismAForm (singletonFilter "term" . maybePrism _PathPiece) mPrev $ aopt (hoistField lift termField) (fslI MsgTerm)
, Just $ prismAForm (singletonFilter "schoolshort" . maybePrism (_PathPiece . from _SchoolId)) mPrev $ aopt (hoistField lift schoolField) (fslI MsgCourseSchool)
, Just $ prismAForm (singletonFilter "lecturer") mPrev $ aopt textField (fslI MsgCourseLecturer)
, Just $ prismAForm (singletonFilter "search") mPrev $ aopt textField (fslI MsgCourseFilterSearch)
, Just $ prismAForm (singletonFilter "openregistration" . maybePrism _PathPiece) mPrev $ fmap (\x -> if isJust x && not (fromJust x) then Nothing else x) . aopt checkBoxField (fslI MsgCourseRegisterOpen)
, muid $> prismAForm (singletonFilter "registered" . maybePrism _PathPiece) mPrev (aopt boolField (fslI MsgCourseFilterRegistered))
]
, dbtStyle = def { dbsFilterLayout = defaultDBSFilterLayout }
, dbtStyle = def
{ dbsFilterLayout = defaultDBSFilterLayout
, dbsTemplate = DBSTCourse (_dbrOutput . _1) (_dbrOutput . _5) (_dbrOutput . _3) (_dbrOutput . _4)
-- ^ course ^ lecturer list ^ isRegistered ^ school
}
, dbtParams = def
, dbtIdent = "courses" :: Text
, dbtCsvEncode = noCsvEncode
@ -208,53 +197,13 @@ getTermCurrentR :: Handler Html
getTermCurrentR = do
termIds <- runDB $ selectKeysList [TermActive ==. True] [Desc TermName]
case fromNullable termIds of
Nothing -> notFound
(Just (maximum -> tid)) ->
redirect $ TermCourseListR tid -- redirect avoids problematic breadcrumbs, headings, etc.
Nothing
-> notFound
Just (maximum -> tid)
-> redirect (CourseListR, [("courses-term", toPathPiece tid)]) -- redirect avoids problematic breadcrumbs, headings, etc.
getTermSchoolCourseListR :: TermId -> SchoolId -> Handler Html
getTermSchoolCourseListR tid ssh = do
void . runDB $ get404 tid -- Just ensure the term exists
School{schoolName=school} <- runDB $ get404 ssh -- Just ensure the term exists
muid <- maybeAuthId
let colonnade = widgetColonnade $ mconcat
[ dbRow
, colCShort
, colDescription
, colRegFrom
, colRegTo
, colMembers
, maybe mempty (const colRegistered) muid
]
whereClause (course, _, _) =
course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
validator = def
& defaultSorting [SortAscBy "cshort"]
coursesTable <- runDB $ makeCourseTable whereClause colonnade validator
defaultLayout $ do
setTitleI $ MsgTermSchoolCourseListTitle tid school
$(widgetFile "courses")
getTermSchoolCourseListR tid ssh = redirect (CourseListR, [("courses-term", toPathPiece tid), ("courses-schoolshort", toPathPiece ssh)])
getTermCourseListR :: TermId -> Handler Html
getTermCourseListR tid = do
void . runDB $ get404 tid -- Just ensure the term exists
muid <- maybeAuthId
let colonnade = widgetColonnade $ mconcat
[ dbRow
, colCShort
, colDescription
, colSchoolShort
, colRegFrom
, colRegTo
, colMembers
, maybe mempty (const colRegistered) muid
]
whereClause (course, _, _) = course E.^. CourseTerm E.==. E.val tid
validator = def
& defaultSorting [SortAscBy "cshort"]
coursesTable <- runDB $ makeCourseTable whereClause colonnade validator
defaultLayout $ do
setTitleI . MsgTermCourseListTitle $ tid
$(widgetFile "courses")
getTermCourseListR tid = redirect (CourseListR, [("courses-term", toPathPiece tid)])

View File

@ -82,10 +82,13 @@ participantInvitationConfig = InvitationConfig{..}
invitationRestriction _ _ = return Authorized
invitationForm (Entity _ Course{..}) _ uid = hoistAForm lift . wFormToAForm $ do
now <- liftIO getCurrentTime
studyFeatures <- wreq (studyFeaturesPrimaryFieldFor False [] $ Just uid)
(fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTooltip) Nothing
studyFeatures <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid)
(fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing
return . fmap (, ()) $ JunctionParticipant <$> pure now <*> studyFeatures <*> pure False
invitationInsertHook _ _ _ _ = id
invitationInsertHook _ _ CourseParticipant{..} _ act = do
res <- act
audit $ TransactionCourseParticipantEdit courseParticipantCourse courseParticipantUser
return res
invitationSuccessMsg (Entity _ Course{..}) _ =
return . SomeMessage $ MsgCourseParticipantInvitationAccepted (CI.original courseName)
invitationUltDest (Entity _ Course{..}) _ = return . SomeRoute $ CourseR courseTerm courseSchool courseShorthand CShowR
@ -165,6 +168,7 @@ postCAddUserR tid ssh csh = do
, courseParticipantAllocated = False
, ..
}
lift . lift . audit $ TransactionCourseParticipantEdit cid uid
return $ case courseParticipantField of
Nothing -> mempty { aurNoUniquePrimaryField = pure userEmail }

View File

@ -1,66 +1,155 @@
module Handler.Course.Register
( ButtonCourseRegister(..)
, CourseRegisterForm(..)
, courseRegisterForm
, getCRegisterR, postCRegisterR
) where
import Import
import Utils.Form
import Utils.Lens
import Handler.Utils
import Data.Function ((&))
import qualified Data.Text as Text
import qualified Data.Conduit.List as C
import Database.Persist.Sql (transactionUndo)
import qualified Database.Esqueleto as E
-- Dedicated CourseRegistrationButton
data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister
data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister | BtnCourseApply | BtnCourseRetractApplication
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonCourseRegister
instance Finite ButtonCourseRegister
nullaryPathPiece ''ButtonCourseRegister $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ButtonCourseRegister id
instance Button UniWorX ButtonCourseRegister where
btnClasses BtnCourseRegister = [BCIsButton, BCPrimary]
btnClasses BtnCourseDeregister = [BCIsButton, BCDanger]
btnClasses BtnCourseRegister = [BCIsButton, BCPrimary]
btnClasses BtnCourseDeregister = [BCIsButton, BCDanger]
btnClasses BtnCourseApply = [BCIsButton, BCPrimary]
btnClasses BtnCourseRetractApplication = [BCIsButton, BCDanger]
btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|]
btnLabel BtnCourseDeregister = [whamlet|#{iconEnrol False} _{MsgBtnCourseDeregister}|]
btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|]
btnLabel BtnCourseDeregister = [whamlet|#{iconEnrol False} _{MsgBtnCourseDeregister}|]
btnLabel BtnCourseApply = [whamlet|#{iconApply True} _{MsgBtnCourseApply}|]
btnLabel BtnCourseRetractApplication = [whamlet|#{iconApply False} _{MsgBtnCourseRetractApplication}|]
data CourseRegisterForm = CourseRegisterForm
{ crfStudyFeatures :: Maybe StudyFeaturesId
, crfApplicationText :: Maybe Text
, crfApplicationFiles :: Maybe (Source Handler File)
}
courseRegisterForm :: (MonadHandler m, HandlerSite m ~ UniWorX) => Entity Course -> m (AForm Handler CourseRegisterForm, ButtonCourseRegister)
-- ^ `CourseRegisterForm` for current user
courseRegisterForm (Entity cid Course{..}) = liftHandlerT $ do
muid <- maybeAuthId
(registration, application) <- runDB $ do
registration <- fmap join . for muid $ getBy . flip UniqueParticipant cid
application <- fmap (listToMaybe =<<) . for muid $ \uid -> selectList [CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid, CourseApplicationAllocation ==. Nothing] []
return (registration, application)
let btn | courseApplicationsRequired
, is _Just application
= BtnCourseRetractApplication
| is _Just registration
= BtnCourseDeregister
| courseApplicationsRequired
= BtnCourseApply
| otherwise
= BtnCourseRegister
isRegistered = btn `elem` [BtnCourseRetractApplication, BtnCourseDeregister]
return . (, btn) . wFormToAForm $ do
MsgRenderer mr <- getMsgRenderer
secretRes <- if
| Just secret <- courseRegisterSecret
, not isRegistered
-> let guardSecret (FormSuccess secret')
| secret == secret' = return $ FormSuccess ()
| otherwise = formFailure [MsgCourseSecretWrong]
guardSecret FormMissing = return FormMissing
guardSecret (FormFailure errs) = return $ FormFailure errs
in guardSecret =<< wreq textField (fslpI MsgCourseSecret $ mr MsgCourseSecret) Nothing
| otherwise
-> return $ FormSuccess ()
fieldRes <- if
| is _Nothing muid
-> return $ FormSuccess Nothing
| is _Just muid
, isRegistered
, Just mFeature <- courseApplicationField . entityVal <$> application
<|> courseParticipantField . entityVal <$> registration
-> wforced (studyFeaturesFieldFor Nothing True (maybeToList mFeature) muid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) mFeature
| otherwise
-> wreq (studyFeaturesFieldFor Nothing False [] muid) (fslI MsgCourseStudyFeature & setTooltip MsgCourseStudyFeatureTip) Nothing
appTextRes <- let fs | courseApplicationsRequired
, is _Just courseApplicationsInstructions
= fslI MsgCourseApplicationText & setTooltip MsgCourseApplicationFollowInstructions
| courseApplicationsRequired
= fslI MsgCourseApplicationText
| is _Just courseApplicationsInstructions
= fslI MsgCourseRegistrationText & setTooltip MsgCourseRegistrationFollowInstructions
| otherwise
= fslI MsgCourseRegistrationText
textField' = convertField unTextarea Textarea textareaField
in if
| not courseApplicationsText
-> return $ FormSuccess Nothing
| is _Just muid
, isRegistered
-> wforced (convertField Just (fromMaybe Text.empty) textField') fs (application >>= courseApplicationText . entityVal)
| otherwise
-> fmap (assertM (not . Text.null) . fmap Text.strip) <$> wopt textField' fs (Just $ application >>= courseApplicationText . entityVal)
hasFiles <- for application $ \(Entity appId _)
-> fmap (not . null) . liftHandlerT . runDB $ selectKeysList [ CourseApplicationFileApplication ==. appId ] [ LimitTo 1 ]
appCID <- for application $ encrypt . entityKey
let appFilesInfo = (,) <$> hasFiles <*> appCID
filesMsg = bool MsgCourseRegistrationFiles MsgCourseApplicationFiles courseApplicationsRequired
if
| isn't _NoUpload courseApplicationsFiles || fromMaybe False hasFiles
-> let filesLinkField = Field{..}
where
fieldParse _ _ = return $ Right Nothing
fieldEnctype = mempty
fieldView theId _ attrs _ _
= [whamlet|
$newline never
$case appFilesInfo
$of Just (True, appCID)
<a ##{theId} *{attrs} href=@{CApplicationR courseTerm courseSchool courseShorthand appCID CAFilesR}>
_{filesMsg}
$of _
<span ##{theId} *{attrs}>
_{MsgCourseApplicationNoFiles}
|]
in void $ wforced filesLinkField (fslI filesMsg) Nothing
| otherwise
-> return ()
appFilesRes <- let mkFs | courseApplicationsRequired = bool MsgCourseApplicationFile MsgCourseApplicationArchive
| otherwise = bool MsgCourseRegistrationFile MsgCourseRegistrationArchive
in if
| isRegistered
-> return $ FormSuccess Nothing
| otherwise
-> aFormToWForm $ fileUploadForm False (fslI . mkFs) courseApplicationsFiles
return $ CourseRegisterForm
<$ secretRes
<*> fieldRes
<*> appTextRes
<*> appFilesRes
-- | Registration button with maybe a userid if logged in
-- , maybe existing features if already registered
-- , maybe some default study features
-- , maybe a course secret
courseRegisterForm :: Maybe UserId -> Maybe CourseParticipant -> Maybe StudyFeaturesId -> Maybe Text -> Form (Maybe StudyFeaturesId, Bool)
-- unfinished WIP: must take study features if registred and show as mforced field
courseRegisterForm loggedin participant defSFid msecret = identifyForm FIDcourseRegister $ \extra -> do
-- secret fields
(msecretRes', msecretView) <- case msecret of
(Just _) | not isRegistered -> bimap Just Just <$> mreq textField (fslpI MsgCourseSecret "Code") Nothing
_ -> return (Nothing,Nothing)
-- study features
(msfRes', msfView) <- case loggedin of
Nothing -> return (Nothing,Nothing)
Just _ -> bimap Just Just <$> case participant of
Just CourseParticipant{courseParticipantField=Just sfid}
-> mforced (studyFeaturesPrimaryFieldFor False [sfid] loggedin) (fslI MsgCourseStudyFeature) (Just sfid)
_other -> mreq (studyFeaturesPrimaryFieldFor False [ ] loggedin) (fslI MsgCourseStudyFeature
& setTooltip MsgCourseStudyFeatureTooltip) (Just defSFid)
-- button de-/register
(btnRes, btnView) <- mreq (buttonField $ bool BtnCourseRegister BtnCourseDeregister isRegistered) "buttonField ignores settings anyway" Nothing
let widget = $(widgetFile "widgets/register-form/register-form")
let msecretRes | Just res <- msecretRes' = Just <$> res
| otherwise = FormSuccess Nothing
let msfRes | Just res <- msfRes' = res
| otherwise = FormSuccess Nothing
-- checks that correct button was pressed, and ignores result of btnRes
let formRes = (,) <$ btnRes <*> msfRes <*> ((==msecret) <$> msecretRes)
return (formRes, widget)
where
isRegistered = isJust participant
-- | Workaround for klicking register button without being logged in.
-- After log in, the user sees a "get request not supported" error.
getCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -76,21 +165,87 @@ getCRegisterR tid ssh csh = do
postCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
postCRegisterR tid ssh csh = do
aid <- requireAuthId
(cid, course, registration) <- runDB $ do
(Entity cid course) <- getBy404 $ TermSchoolCourseShort tid ssh csh
registration <- getBy (UniqueParticipant aid cid)
return (cid, course, entityVal <$> registration)
let isRegistered = isJust registration
((regResult,_), _) <- runFormPost $ courseRegisterForm (Just aid) registration Nothing $ courseRegisterSecret course
formResult regResult $ \(mbSfId,codeOk) -> if
| isRegistered -> do
runDB $ deleteBy $ UniqueParticipant aid cid
addMessageIconI Info IconEnrolFalse MsgCourseDeregisterOk
| codeOk -> do
actTime <- liftIO getCurrentTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime mbSfId False
when (isJust regOk) $ addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk
| otherwise -> addMessageI Warning MsgCourseSecretWrong
-- addMessage Info $ toHtml $ show regResult -- For debugging only
uid <- requireAuthId
course@(Entity cid Course{..}) <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
(courseRegisterForm', courseRegisterButton) <- courseRegisterForm course
((regResult,_), _) <- runFormPost $ renderAForm FormStandard courseRegisterForm'
formResult regResult $ \CourseRegisterForm{..} -> do
cTime <- liftIO getCurrentTime
let
mkApplication
| courseApplicationsRequired || is _Just (void crfApplicationText <|> void crfApplicationFiles)
= void <$> do
appIds <- selectKeysList [ CourseApplicationAllocation ==. Nothing, CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] []
appRes <- case appIds of
[] -> insertUnique $ CourseApplication cid uid crfStudyFeatures crfApplicationText Nothing Nothing Nothing Nothing cTime
(prevId:ps) -> do
forM_ ps $ \appId -> do
deleteApplicationFiles appId
delete appId
audit $ TransactionCourseApplicationDeleted cid uid appId
deleteApplicationFiles prevId
update prevId [ CourseApplicationField =. crfStudyFeatures, CourseApplicationText =. crfApplicationText, CourseApplicationTime =. cTime ]
return $ Just prevId
whenIsJust appRes $
audit . TransactionCourseApplicationEdit cid uid
whenIsJust ((,) <$> appRes <*> crfApplicationFiles) $ \(appId, fSource) -> do
runConduit $ transPipe liftHandlerT fSource .| C.mapM_ (\f -> insert f >>= insert_ . CourseApplicationFile appId)
return appRes
| otherwise
= return $ Just ()
mkRegistration = do
audit $ TransactionCourseParticipantEdit cid uid
insertUnique $ CourseParticipant cid uid cTime crfStudyFeatures False
deleteApplications = do
appIds <- selectKeysList [ CourseApplicationAllocation ==. Nothing, CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] []
forM_ appIds $ \appId -> do
deleteApplicationFiles appId
delete appId
audit $ TransactionCourseApplicationDeleted cid uid appId
deleteApplicationFiles appId = do
fs <- selectList [ CourseApplicationFileApplication ==. appId ] []
deleteCascadeWhere [ FileId <-. map (courseApplicationFileFile . entityVal) fs ]
case courseRegisterButton of
BtnCourseRegister -> runDB $ do
regOk <- (\app reg -> (,) <$> app <*> reg) <$> mkApplication <*> mkRegistration
case regOk of
Nothing -> transactionUndo
Just _ -> addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk
BtnCourseDeregister -> runDB $ do
deleteApplications
deleteBy $ UniqueParticipant uid cid
audit $ TransactionCourseParticipantDeleted cid uid
examRegistrations <- E.select . E.from $ \(examRegistration `E.InnerJoin` exam) -> do
E.on $ examRegistration E.^. ExamRegistrationExam E.==. exam E.^. ExamId
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
E.&&. examRegistration E.^. ExamRegistrationUser E.==. E.val uid
return examRegistration
forM_ examRegistrations $ \(Entity erId ExamRegistration{..}) -> do
delete erId
audit $ TransactionExamDeregister examRegistrationExam uid
examResults <- E.select . E.from $ \(examResult `E.InnerJoin` exam) -> do
E.on $ examResult E.^. ExamResultExam E.==. exam E.^. ExamId
E.where_ $ exam E.^. ExamCourse E.==. E.val cid
E.&&. examResult E.^. ExamResultUser E.==. E.val uid
return examResult
forM_ examResults $ \(Entity erId ExamResult{..}) -> do
delete erId
audit $ TransactionExamResultDeleted examResultExam uid
addMessageIconI Info IconEnrolFalse MsgCourseDeregisterOk
BtnCourseApply -> runDB $ do
regOk <- mkApplication
case regOk of
Nothing -> transactionUndo
Just _ -> addMessageIconI Success IconApplyTrue MsgCourseApplyOk
BtnCourseRetractApplication -> runDB $ do
deleteApplications
addMessageIconI Info IconApplyFalse MsgCourseRetractApplyOk
redirect $ CourseR tid ssh csh CShowR

View File

@ -1,5 +1,6 @@
module Handler.Course.Show
( getCShowR
, getCRegisterTemplateR
) where
import Import
@ -11,7 +12,7 @@ import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH
import qualified Data.CaseInsensitive as CI
import Data.Function ((&))
import Utils.Lens
import qualified Data.Map as Map
@ -19,11 +20,15 @@ import qualified Database.Esqueleto as E
import Handler.Course.Register
import System.FilePath (addExtension)
import qualified Data.Conduit.List as C
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do
mbAid <- maybeAuthId
(cid,course,schoolName,participants,registration,defSFid,lecturers,assistants,correctors,tutors,mAllocation) <- runDB . maybeT notFound $ do
(cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication) <- runDB . maybeT notFound $ do
[(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
<- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do
@ -38,7 +43,6 @@ getCShowR tid ssh csh = do
E.where_ $ part E.^. CourseParticipantCourse E.==. course E.^. CourseId
return ( E.countRows :: E.SqlExpr (E.Value Int))
return (course,school E.^. SchoolName, numParticipants, participant)
defSFid <- ifMaybeM mbAid Nothing $ \uid -> lift $ selectFirst [StudyFeaturesUser ==. uid, StudyFeaturesType ==. FieldPrimary, StudyFeaturesValid ==. True] [Desc StudyFeaturesUpdated, Desc StudyFeaturesDegree, Desc StudyFeaturesField] -- sorting by degree & field is an heuristic only, but this is okay for a default suggestion
staff <- lift . E.select $ E.from $ \(lecturer `E.InnerJoin` user) -> do
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid
@ -66,19 +70,27 @@ getCShowR tid ssh csh = do
E.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cid
E.limit 1
return allocation
return (cid,course,schoolName,participants,registration,entityKey <$> defSFid,lecturers,assistants,correctors,tutors,mAllocation)
hasApplicationTemplate <- lift . E.selectExists . E.from $ \courseAppInstructionFile ->
E.where_ $ courseAppInstructionFile E.^. CourseAppInstructionFileCourse E.==. E.val cid
mApplication <- lift . fmap (listToMaybe =<<) . for mbAid $ \uid -> selectList [CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid, CourseApplicationAllocation ==. Nothing] []
return (cid,course,schoolName,participants,registration,lecturers,assistants,correctors,tutors,mAllocation,hasApplicationTemplate,mApplication)
mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course
mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration
(regWidget, regEnctype) <- generateFormPost $ courseRegisterForm mbAid registration defSFid $ courseRegisterSecret course
let regForm = wrapForm regWidget def
{ formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR
, formEncoding = regEnctype
, formSubmit = FormNoSubmit
}
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True
regForm <- if
| is _Just mbAid -> do
(courseRegisterForm', regButton) <- courseRegisterForm (Entity cid course)
(regWidget, regEnctype) <- generateFormPost $ renderAForm FormStandard courseRegisterForm'
return $ wrapForm' regButton regWidget def
{ formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR
, formEncoding = regEnctype
, formSubmit = FormSubmit
}
| otherwise
-> return . modal $(widgetFile "course/login-to-register") . Left . SomeRoute $ AuthR LoginR
registrationOpen <- hasWriteAccessTo $ CourseR tid ssh csh CRegisterR
let
tutorialDBTable = DBTable{..}
@ -224,3 +236,15 @@ getCShowR tid ssh csh = do
siteLayout (toWgt $ courseName course) $ do
setTitleI $ prependCourseTitle tid ssh csh (""::Text)
$(widgetFile "course")
getCRegisterTemplateR :: TermId -> SchoolId -> CourseShorthand -> Handler TypedContent
getCRegisterTemplateR tid ssh csh = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgCourseApplicationTemplateArchiveName tid ssh csh
let source = (.| C.map entityVal) . E.selectSource . E.from $ \(file `E.InnerJoin` courseAppInstructionFile `E.InnerJoin` course) -> do
E.on $ course E.^. CourseId E.==. courseAppInstructionFile E.^. CourseAppInstructionFileCourse
E.on $ courseAppInstructionFile E.^. CourseAppInstructionFileFile E.==. file E.^. FileId
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
return file
serveSomeFiles archiveName source

View File

@ -95,7 +95,7 @@ postCUserR tid ssh csh uCId = do
((regFieldRes, regFieldView), regFieldEnctype) <- runFormPost . identifyForm FIDcRegField $ \csrf ->
let currentField :: Maybe (Maybe StudyFeaturesId)
currentField = courseParticipantField . entityVal <$> mRegistration
in over _2 ((toWidget csrf <>) . fvInput) <$> mreq (studyFeaturesPrimaryFieldFor True (maybeToList $ join currentField) $ Just uid) ("" & addAutosubmit) currentField
in over _2 ((toWidget csrf <>) . fvInput) <$> mreq (studyFeaturesFieldFor Nothing True (maybeToList $ join currentField) $ Just uid) ("" & addAutosubmit) currentField
let registrationFieldFrag :: Text
registrationFieldFrag = "registration-field"
@ -111,6 +111,7 @@ postCUserR tid ssh csh uCId = do
formResult regFieldRes $ \courseParticipantField' -> do
runDB $ do
update pId [ CourseParticipantField =. courseParticipantField' ]
audit $ TransactionCourseParticipantEdit cid uid
addMessageI Success MsgCourseStudyFeatureUpdated
redirect $ currentRoute :#: registrationFieldFrag
@ -140,17 +141,22 @@ postCUserR tid ssh csh uCId = do
-> invalidArgs ["User not registered"]
BtnCourseRegister -> do
now <- liftIO getCurrentTime
let primaryField
| [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesType == FieldPrimary && studyFeaturesValid) studies
let field
| [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesValid) studies
= Just featId
| otherwise
= Nothing
pId <- runDB . insertUnique $ CourseParticipant cid uid now primaryField False
pId <- runDB $ do
pId <- insertUnique $ CourseParticipant cid uid now field False
when (is _Just pId) $
audit $ TransactionCourseParticipantEdit cid uid
return pId
case pId of
Just _ -> do
addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk
redirect currentRoute
Nothing -> invalidArgs ["User already registered"]
_other -> fail "Invalid @regButton@"
mRegAt <- for (courseParticipantRegistration . entityVal <$> mRegistration) $ formatTime SelFormatDateTime

View File

@ -251,10 +251,14 @@ postCUsersR tid ssh csh = do
cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser]
redirect (CourseR tid ssh csh CCommR, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cids])
(CourseUserDeregister,selectedUsers) -> do
nrDel <- runDB $ deleteWhereCount
[ CourseParticipantCourse ==. cid
, CourseParticipantUser <-. Set.toList selectedUsers
]
Sum nrDel <- fmap mconcat . runDB . forM (Set.toList selectedUsers) $ \uid -> do
nrDel <- deleteWhereCount
[ CourseParticipantCourse ==. cid
, CourseParticipantUser ==. uid
]
unless (nrDel == 0) $
audit $ TransactionCourseParticipantDeleted cid uid
return $ Sum nrDel
addMessageI Success $ MsgCourseUsersDeregistered nrDel
redirect $ CourseR tid ssh csh CUsersR
let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]

View File

@ -149,6 +149,7 @@ postEAddUserR tid ssh csh examn = do
, courseParticipantAllocated = False
, ..
}
lift . lift . audit $ TransactionCourseParticipantEdit cid uid
lift $ lift examRegister
return $ case courseParticipantField of

View File

@ -90,12 +90,13 @@ examRegistrationInvitationConfig = InvitationConfig{..}
case (isRegistered, invDBExamRegistrationCourseRegister) of
(False, False) -> permissionDeniedI MsgUnauthorizedParticipant
(False, True ) -> do
fieldRes <- wreq (studyFeaturesPrimaryFieldFor False [] $ Just uid) (fslI MsgCourseStudyFeature) Nothing
fieldRes <- wreq (studyFeaturesFieldFor Nothing False [] $ Just uid) (fslI MsgCourseStudyFeature) Nothing
return $ (JunctionExamRegistration invDBExamRegistrationOccurrence now, ) . Just <$> fieldRes
(True , _ ) -> return $ pure (JunctionExamRegistration invDBExamRegistrationOccurrence now, Nothing)
invitationInsertHook (Entity eid Exam{..}) _ ExamRegistration{..} mField act = do
whenIsJust mField $ \cpField ->
whenIsJust mField $ \cpField -> do
insert_ $ CourseParticipant examCourse examRegistrationUser examRegistrationTime cpField False
audit $ TransactionCourseParticipantEdit examCourse examRegistrationUser
let doAudit = audit $ TransactionExamRegister eid examRegistrationUser
act <* doAudit

View File

@ -443,6 +443,7 @@ postEUsersR tid ssh csh examn = do
, courseParticipantField = examUserCsvActCourseField
, courseParticipantAllocated = False
}
audit $ TransactionCourseParticipantEdit examCourse examUserCsvActUser
insert_ ExamRegistration
{ examRegistrationExam = eid
, examRegistrationUser = examUserCsvActUser
@ -461,8 +462,10 @@ postEUsersR tid ssh csh examn = do
audit $ TransactionExamRegister eid examUserCsvActUser
ExamUserCsvAssignOccurrenceData{..} ->
update examUserCsvActRegistration [ ExamRegistrationOccurrence =. examUserCsvActOccurrence ]
ExamUserCsvSetCourseFieldData{..} ->
ExamUserCsvSetCourseFieldData{..} -> do
update examUserCsvActCourseParticipant [ CourseParticipantField =. examUserCsvActCourseField ]
CourseParticipant{..} <- getJust examUserCsvActCourseParticipant
audit $ TransactionCourseParticipantEdit examCourse courseParticipantUser
ExamUserCsvSetResultData{..} -> case examUserCsvActExamResult of
Nothing -> do
deleteBy $ UniqueExamResult eid examUserCsvActUser
@ -481,6 +484,10 @@ postEUsersR tid ssh csh examn = do
ExamRegistration{examRegistrationUser} <- getJust examUserCsvActRegistration
audit $ TransactionExamDeregister eid examRegistrationUser
delete examUserCsvActRegistration
result <- getBy $ UniqueExamResult eid examRegistrationUser
forM_ result $ \(Entity erId _) -> do
delete erId
audit $ TransactionExamResultDeleted eid examRegistrationUser
ExamUserCsvSetCourseNoteData{ examUserCsvActCourseNote = Nothing, .. } -> do
noteId <- getKeyBy $ UniqueCourseUserNote examUserCsvActUser examCourse
whenIsJust noteId $ \nid -> do
@ -631,7 +638,6 @@ postEUsersR tid ssh csh examn = do
, (studyFeatures E.^. StudyFeaturesSemester E.==.) . E.val <$> csvEUserSemester
]
E.where_ $ studyFeatures E.^. StudyFeaturesUser E.==. E.val uid
E.&&. studyFeatures E.^. StudyFeaturesType E.==. E.val FieldPrimary
E.&&. studyFeatures E.^. StudyFeaturesValid E.==. E.val True
E.limit 2
return $ studyFeatures E.^. StudyFeaturesId

View File

@ -33,9 +33,10 @@ getInfoR :: Handler Html
getInfoR = -- do
siteLayoutMsg MsgInfoHeading $ do
setTitleI MsgInfoHeading
let features = $(i18nWidgetFile "featureList")
changeLog = $(i18nWidgetFile "changelog")
knownBugs = $(i18nWidgetFile "knownBugs")
let features = $(i18nWidgetFile "featureList")
changeLog = $(i18nWidgetFile "changelog")
knownBugs = $(i18nWidgetFile "knownBugs")
implementation = $(i18nWidgetFile "implementation")
gitInfo :: Text
gitInfo = $gitDescribe <> " (" <> $gitCommitDate <> ")"
$(widgetFile "versionHistory")

View File

@ -23,6 +23,8 @@ import Handler.Utils.Table.Columns
import Control.Monad.Writer (MonadWriter(..), execWriterT)
import System.FilePath (addExtension)
data MaterialForm = MaterialForm
{ mfName :: MaterialName
@ -358,16 +360,19 @@ postMDelR tid ssh csh mnm = do
-- | Serve all material-files
getMArchiveR :: TermId -> SchoolId -> CourseShorthand -> MaterialName -> Handler TypedContent
getMArchiveR tid ssh csh mnm = serveSomeFiles archivename getMatQuery
where
archivename = unpack (termToText (unTermKey tid) <> "-" <> toPathPiece (unSchoolKey ssh <> "-" <> csh <> "-" <> mnm)) <.> "zip"
getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $
\(course `E.InnerJoin` material `E.InnerJoin` materialFile `E.InnerJoin` file) -> do
E.on $ file E.^. FileId E.==. materialFile E.^. MaterialFileFile
E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial
E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm
return file
getMArchiveR tid ssh csh mnm = do
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgMaterialArchiveName tid ssh csh mnm
let getMatQuery = (.| C.map entityVal) . E.selectSource . E.from $
\(course `E.InnerJoin` material `E.InnerJoin` materialFile `E.InnerJoin` file) -> do
E.on $ file E.^. FileId E.==. materialFile E.^. MaterialFileFile
E.on $ material E.^. MaterialId E.==. materialFile E.^. MaterialFileMaterial
E.on $ material E.^. MaterialCourse E.==. course E.^. CourseId
E.where_ $ course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. material E.^. MaterialName E.==. E.val mnm
return file
serveSomeFiles archiveName getMatQuery

View File

@ -89,7 +89,7 @@ notificationForm :: Maybe NotificationSettings -> AForm Handler NotificationSett
notificationForm template = wFormToAForm $ do
mbUid <- liftHandlerT maybeAuthId
isAdmin <- hasReadAccessTo AdminR
let
sectionIsHidden :: NotificationTriggerKind -> DB Bool
sectionIsHidden nt
@ -117,13 +117,13 @@ notificationForm template = wFormToAForm $ do
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val uid
| otherwise
= return False
ntHidden <- liftHandlerT . runDB
$ Set.fromList universeF
& Map.fromSet sectionIsHidden
& sequenceA
& fmap (!)
let
nsForm nt
| maybe False ntHidden $ ntSection nt
@ -147,7 +147,7 @@ notificationForm template = wFormToAForm $ do
-- _other -> Nothing
forcedTriggers = [NTUserRightsUpdate, NTUserAuthModeUpdate]
aFormToWForm $ NotificationSettings <$> sectionedFuncForm ntSection nsForm (fslI MsgNotificationSettings) False
@ -274,7 +274,7 @@ makeProfileData (Entity uid User{..}) = do
submissionTable <- mkSubmissionTable uid -- Tabelle mit allen Abgaben und Abgabe-Gruppen
submissionGroupTable <- mkSubmissionGroupTable uid -- Tabelle mit allen Abgabegruppen
correctionsTable <- mkCorrectionsTable uid -- Tabelle mit allen Korrektor-Aufgaben
let examTable = [whamlet|Klausuren werden momentan leider noch nicht unterstützt.|]
let examTable = [whamlet|Prüfungen werden hier momentan leider noch nicht unterstützt.|]
let ownTutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
let tutorialTable = [whamlet|Übungsgruppen werden momentan leider noch nicht unterstützt.|]
lastLogin <- traverse (formatTime SelFormatDateTime) userLastAuthentication

View File

@ -60,6 +60,8 @@ import Utils.Sql
import Data.Aeson hiding (Result(..))
import Text.Hamlet (ihamlet)
import System.FilePath (addExtension)
{-
* Implement Handlers
@ -439,9 +441,8 @@ getSShowR tid ssh csh shn = do
getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
getSArchiveR tid ssh csh shn = do
MsgRenderer mr <- getMsgRenderer
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <.> "zip"
sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetArchiveName tid ssh csh shn
let sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound]
serveZipArchive archiveName $ sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal
@ -476,8 +477,8 @@ getSFileR tid ssh csh shn sft file = serveOneFile $ sheetFileQuery tid ssh csh s
getSZipR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> Handler TypedContent
getSZipR tid ssh csh shn sft = do
MsgRenderer mr <- getMsgRenderer
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <> "_" <> (unpack $ toPathPiece sft) <.> "zip"
sft' <- ap getMessageRender $ pure sft
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetTypeArchiveName tid ssh csh shn sft'
serveSomeFiles archiveName $ sheetFilesAllQuery tid ssh csh shn sft .| C.map entityVal

View File

@ -45,6 +45,8 @@ import Text.Hamlet (ihamlet)
-- import qualified Yesod.Colonnade as Yesod
-- import qualified Text.Blaze.Html5.Attributes as HA
import System.FilePath (addExtension)
-- DEPRECATED: We always show all edits!
-- numberOfSubmissionEditDates :: Int64
-- numberOfSubmissionEditDates = 3 -- for debugging only, should be 1 in production.
@ -124,26 +126,9 @@ submissionUserInvitationConfig = InvitationConfig{..}
makeSubmissionForm :: CourseId -> Maybe SubmissionId -> UploadMode -> SheetGroup -> Bool -> Set (Either UserEmail UserId) -> Form (Maybe (Source Handler File), Set (Either UserEmail UserId))
makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
<$> fileUploadForm
<$> fileUploadForm (is _Just msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
<*> wFormToAForm submittorsForm
where
fileUploadForm = case uploadMode of
NoUpload
-> pure Nothing
UploadAny{..}
-> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt $ isJust msmid) (zipFileField unpackZips extensionRestriction) (fsm $ bool MsgSubmissionFile MsgSubmissionArchive unpackZips) Nothing
UploadSpecific{..}
-> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles)
specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe (Source Handler File))
specificFileForm spec@UploadSpecificFile{..}
= bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing
mergeFileSources :: [Maybe (Source Handler File)] -> Maybe (Source Handler File)
mergeFileSources (catMaybes -> sources) = case sources of
[] -> Nothing
fs -> Just $ sequence_ fs
miCell' :: Markup -> Either UserEmail UserId -> Widget
miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation")
miCell' csrf (Right uid) = do
@ -574,11 +559,10 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $
guardAuthResult =<< evalAccess (CSubmissionR tid ssh csh shn cID CorrectionR) False
let filename
| SubmissionOriginal <- sfType = toPathPiece cID <> "-" <> toPathPiece sfType
| otherwise = toPathPiece cID
sfType' <- ap getMessageRender $ pure sfType
archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgSubmissionTypeArchiveName tid ssh csh shn cID sfType'
source = do
let source = do
submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID
rating <- lift $ getRating submissionID
@ -593,7 +577,7 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating
serveSomeFiles (unpack filename <.> "zip") source
serveSomeFiles archiveName source
getSubDelR, postSubDelR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> Handler Html
getSubDelR = postSubDelR

View File

@ -75,7 +75,7 @@ serveSomeFiles archiveName source = do
[file] -> sendThisFile file
_moreFiles -> do
setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do
respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
@ -92,7 +92,7 @@ serveZipArchive archiveName source = do
[] -> notFound
_moreFiles -> do
setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do
respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder

View File

@ -32,7 +32,6 @@ import qualified Data.Map as Map
import qualified Data.Vector as Vector
import qualified Data.HashMap.Strict as HashMap
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Attoparsec.ByteString.Lazy as A
@ -43,8 +42,8 @@ instance Exception CsvParseError
typeCsv, typeCsv' :: ContentType
typeCsv = "text/csv"
typeCsv' = BS.intercalate "; " [typeCsv, "charset=UTF-8", "header=present"]
typeCsv = simpleContentType typeCsv'
typeCsv' = "text/csv; charset=UTF-8; header=present"
extensionCsv :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]

View File

@ -25,6 +25,7 @@ import Handler.Utils.Zip
import qualified Data.Conduit.List as C
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Data.Set (Set)
import qualified Data.Set as Set
@ -312,6 +313,9 @@ termsAllowedField = selectField $ do
| otherwise = [TermActive ==. True]
optionsPersistKey termFilter [Desc TermStart] termName
termField :: Field Handler TermId
termField = selectField $ optionsPersistKey [] [Asc TermName] termName
termsSetField :: [TermId] -> Field Handler TermId
termsSetField tids = selectField $ optionsPersistKey [TermName <-. (unTermKey <$> tids)] [Desc TermStart] termName
-- termsSetField tids = selectFieldList [(unTermKey t, t)| t <- tids ]
@ -334,25 +338,35 @@ schoolFieldEnt = selectField $ optionsPersist [] [Asc SchoolName] schoolName
schoolFieldFor :: [SchoolId] -> Field Handler SchoolId
schoolFieldFor userSchools = selectField $ optionsPersistKey [SchoolShorthand <-. map unSchoolKey userSchools] [Asc SchoolName] schoolName
-- | Select one of the user's primary active courses, or from a given list of StudyFeatures (regardless of user)
-- | Select one of the user's primary active study features, or from a given list of StudyFeatures (regardless of user)
studyFeaturesPrimaryFieldFor :: Bool -- ^ Allow user to select `Nothing` (only applies if set of options is nonempty)?
-> [StudyFeaturesId] -> Maybe UserId -> Field Handler (Maybe StudyFeaturesId)
studyFeaturesPrimaryFieldFor isOptional oldFeatures mbuid = selectField $ do
{-# DEPRECATED studyFeaturesPrimaryFieldFor "Use studyFeaturesFieldFor" #-}
studyFeaturesPrimaryFieldFor = studyFeaturesFieldFor . Just $ Set.singleton FieldPrimary
-- | Select one of the user's active study features, or from a given list of StudyFeatures (regardless of user)
studyFeaturesFieldFor :: Maybe (Set StudyFieldType) -- ^ Optionally restrict fields to only given types
-> Bool -- ^ Allow user to select `Nothing` (only applies if set of options is nonempty)?
-> [StudyFeaturesId] -> Maybe UserId -> Field Handler (Maybe StudyFeaturesId)
studyFeaturesFieldFor mRestr isOptional oldFeatures mbuid = selectField $ do
-- we need a join, so we cannot just use optionsPersistCryptoId
rawOptions <- runDB $ E.select $ E.from $ \(feature `E.InnerJoin` degree `E.InnerJoin` field) -> do
E.on $ feature E.^. StudyFeaturesField E.==. field E.^. StudyTermsId
E.on $ feature E.^. StudyFeaturesDegree E.==. degree E.^. StudyDegreeId
E.where_ $ ((feature E.^. StudyFeaturesId) `E.in_` E.valList oldFeatures)
E.||. isPrimaryActiveUserStudyFeature feature
E.||. (isActiveUserStudyFeature feature E.&&. isCorrectType feature)
return (feature E.^. StudyFeaturesId, degree, field)
MsgRenderer mr <- getMsgRenderer
mkOptionList . nonEmptyOptions (mr MsgNoPrimaryStudyField) <$> mapM (procOptions mr) rawOptions
mkOptionList . nonEmptyOptions (mr MsgNoStudyField) <$> mapM (procOptions mr) rawOptions
where
isPrimaryActiveUserStudyFeature feature = case mbuid of
Nothing -> E.val False
(Just uid) -> feature E.^. StudyFeaturesUser E.==. E.val uid
E.&&. feature E.^. StudyFeaturesValid E.==. E.val True
E.&&. feature E.^. StudyFeaturesType E.==. E.val FieldPrimary
isActiveUserStudyFeature feature = case mbuid of
Nothing -> E.false
Just uid -> feature E.^. StudyFeaturesUser E.==. E.val uid
E.&&. feature E.^. StudyFeaturesValid
isCorrectType feature = case mRestr of
Nothing -> E.true
Just restr -> feature E.^. StudyFeaturesType `E.in_` E.valList (Set.toList restr)
procOptions :: (StudyDegreeTerm -> Text) -> (E.Value StudyFeaturesId, Entity StudyDegree, Entity StudyTerms) -> Handler (Option (Maybe StudyFeaturesId))
procOptions mr (E.Value sfid, Entity _dgid sdegree, Entity _stid sterm) = do
@ -372,7 +386,7 @@ studyFeaturesPrimaryFieldFor isOptional oldFeatures mbuid = selectField $ do
nullOption = Option
{ optionDisplay = emptyOpt
, optionInternalValue = Nothing
, optionExternalValue = "NoPrimaryStudyField"
, optionExternalValue = "NoStudyField"
}
@ -384,7 +398,7 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
[ ( UploadModeNone, pure NoUpload)
, ( UploadModeAny
, UploadAny
<$> apreq checkBoxField (fslI MsgUploadModeUnpackZips & setTooltip MsgUploadModeUnpackZipsTip) (prev ^? _Just . _unpackZips)
<$> (fromMaybe False <$> aopt checkBoxField (fslI MsgUploadModeUnpackZips & setTooltip MsgUploadModeUnpackZipsTip) (Just $ prev ^? _Just . _unpackZips))
<*> aopt extensionRestrictionField (fslI MsgUploadModeExtensionRestriction & setTooltip MsgUploadModeExtensionRestrictionTip) ((prev ^? _Just . _extensionRestriction) <|> fmap Just defaultExtensionRestriction)
)
, ( UploadModeSpecific
@ -455,6 +469,8 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/uploadSpecificFiles/layout")
submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode
submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev
where
@ -645,12 +661,37 @@ zipFileField doUnpack permittedExtensions = Field{..}
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField")
zipExtensions = mimeExtensions "application/zip"
zipExtensions = mimeExtensions typeZip
acceptRestricted = isJust permittedExtensions
accept = Text.intercalate "," . map ("." <>) $ bool [] (Set.toList zipExtensions) doUnpack ++ toListOf (_Just . re _nullable . folded) permittedExtensions
multiFileField :: Handler (Set FileId) -> Field Handler (Source Handler (Either FileId File))
fileUploadForm :: Bool -- ^ Required?
-> (Bool -> FieldSettings UniWorX) -- ^ given @unpackZips@ generate `FieldSettings` in the case of `UploadAny`
-> UploadMode -> AForm Handler (Maybe (Source Handler File))
fileUploadForm isReq mkFs = \case
NoUpload
-> pure Nothing
UploadAny{..}
-> (bool (\f fs _ -> Just <$> areq f fs Nothing) aopt isReq) (zipFileField unpackZips extensionRestriction) (mkFs unpackZips) Nothing
UploadSpecific{..}
-> mergeFileSources <$> sequenceA (map specificFileForm . Set.toList $ toNullable specificFiles)
where
specificFileForm :: UploadSpecificFile -> AForm Handler (Maybe (Source Handler File))
specificFileForm spec@UploadSpecificFile{..}
= bool (\f fs d -> aopt f fs $ fmap Just d) (\f fs d -> Just <$> areq f fs d) specificFileRequired (specificFileField spec) (fsl specificFileLabel) Nothing
mergeFileSources :: [Maybe (Source Handler File)] -> Maybe (Source Handler File)
mergeFileSources (catMaybes -> sources) = case sources of
[] -> Nothing
fs -> Just $ sequence_ fs
multiFileField' :: Source Handler (Either FileId File) -- ^ Permitted files in same format as produced by `multiFileField`
-> Field Handler (Source Handler (Either FileId File))
multiFileField' permittedFiles = multiFileField . runConduit $ permittedFiles .| C.mapMaybe (preview _Left) .| C.foldMap Set.singleton
multiFileField :: Handler (Set FileId) -- ^ Set of files that may be submitted by id-reference
-> Field Handler (Source Handler (Either FileId File))
multiFileField permittedFiles' = Field{..}
where
fieldEnctype = Multipart

View File

@ -25,12 +25,11 @@ gradeSummaryWidget title sts =
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
hasPoints = positiveSum $ numSheetsPoints sumSummaries
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row")
| (sumHeader,summary) <-
[ (MsgSheetTypeNormal' ,normalSummary)
, (MsgSheetTypeBonus' ,bonusSummary)
, (MsgSheetTypeInformational' ,informationalSummary)
] ]
rowWgt (sumHeader, summary) = $(widgetFile "widgets/grading-summary/grading-summary-row") -- diese Funktonsdefinition darf leider nicht im .hamlet stehen!
rowsShown = [ (MsgSheetTypeNormal' ,normalSummary)
, (MsgSheetTypeBonus' ,bonusSummary)
, (MsgSheetTypeInformational' ,informationalSummary)
] -- diese Liste könnte auch im .hamlet definiert werden
in if 0 == numSheets sumSummaries
then mempty
else $(widgetFile "widgets/grading-summary/grading-summary")

View File

@ -279,8 +279,9 @@ submissionMultiArchive (Set.toList -> ids) = do
execWriter . forM ratedSubmissions $ \(_rating,_submission,(shn,csh,ssh,tid)) ->
tell (Set.singleton shn, Set.singleton csh, Set.singleton ssh, Set.singleton tid)
setContentDisposition' $ Just "submissions.zip"
(<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do
archiveName <- ap getMessageRender $ pure MsgSubmissionArchiveName
setContentDisposition' $ Just ((addExtension `on` unpack) archiveName extensionZip)
(<* cleanup) . respondSource typeZip . transPipe (runDBRunner dbrunner) $ do
let
fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File
fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do

View File

@ -34,6 +34,7 @@ module Handler.Utils.Table.Pagination
, (&)
, module Control.Monad.Trans.Maybe
, module Colonnade
, DBSTemplateMode(..)
) where
import Handler.Utils.Table.Pagination.Types
@ -42,7 +43,7 @@ import Handler.Utils.Form
import Handler.Utils.Csv
import Handler.Utils.ContentDisposition
import Utils
import Utils.Lens.TH
import Utils.Lens
import Import hiding (pi)
import qualified Database.Esqueleto as E
@ -79,7 +80,6 @@ import Text.Hamlet (hamletFile)
import Data.Ratio ((%))
import Control.Lens hiding ((<.>))
import Control.Lens.Extras (is)
import Data.List (elemIndex)
@ -104,6 +104,7 @@ import Data.Semigroup as Sem (Semigroup(..))
import qualified Data.Conduit.List as C
import Handler.Utils.DateTime (formatTimeW)
import qualified Control.Monad.Catch as Catch
@ -428,7 +429,7 @@ data DBEmptyStyle = DBESNoHeading | DBESHeading
instance Default DBEmptyStyle where
def = DBESHeading
data DBStyle = DBStyle
data DBStyle r = DBStyle
{ dbsEmptyStyle :: DBEmptyStyle
, dbsEmptyMessage :: UniWorXMessage
, dbsAttrs :: [(Text, Text)]
@ -438,9 +439,13 @@ data DBStyle = DBStyle
-> Widget
-> Widget
-- ^ Filter UI, Filter Encoding, Filter action, table
, dbsTemplate :: DBSTemplateMode r
}
instance Default DBStyle where
data DBSTemplateMode r = DBSTDefault
| DBSTCourse (Lens' r (Entity Course)) (Lens' r [Entity User]) (Lens' r Bool) (Lens' r (Entity School))
instance Default (DBStyle r) where
def = DBStyle
{ dbsEmptyStyle = def
, dbsEmptyMessage = MsgNoTableContent
@ -451,6 +456,7 @@ instance Default DBStyle where
<!-- No Filter UI -->
^{scrolltable}
|]
, dbsTemplate = DBSTDefault
}
defaultDBSFilterLayout :: Widget -- ^ Filter UI
@ -523,7 +529,7 @@ data DBTable m x = forall a r r' h i t k k' csv.
, dbtSorting :: Map SortingKey (SortColumn t)
, dbtFilter :: Map FilterKey (FilterColumn t)
, dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
, dbtStyle :: DBStyle
, dbtStyle :: DBStyle r'
, dbtParams :: DBParams m x
, dbtCsvEncode :: DBTCsvEncode r' csv
, dbtCsvDecode :: Maybe (DBTCsvDecode r' k' csv)
@ -1024,29 +1030,40 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
. setParam (wIdent "pagination") Nothing
table' :: HandlerSite m ~ UniWorX => WriterT x m Widget
table' = do
let
table' = let
columnCount :: Int64
columnCount = olength64 $ getColonnade dbtColonnade
genHeaders SortableP{..} = forM (toSortable . oneColonnadeHead <$> getColonnade dbtColonnade) $ \Sortable{..} -> do
widget <- sortableContent ^. cellContents
let
directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ]
isSortable = isJust sortableKey
isSorted = (`elem` directions)
isSorted dir = fromMaybe False $ (==) <$> (SortingSetting <$> sortableKey <*> pure dir) <*> listToMaybe psSorting
attrs = sortableContent ^. cellAttrs
piSorting' = [ sSet | sSet <- fromMaybe [] piSorting, Just (sortKey sSet) /= sortableKey ]
return $(widgetFile "table/cell/header")
columnCount :: Int64
columnCount = olength64 $ getColonnade dbtColonnade
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
wRows <- forM rows $ \row' -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row') -> cell') -> do
widget <- cell' ^. cellContents
let attrs = cell' ^. cellAttrs
return $(widgetFile "table/cell/body")
return $(widgetFile "table/colonnade")
case dbsTemplate of
DBSTCourse _ _ _ _ -> return $(widgetFile "table/course/header")
DBSTDefault -> return $(widgetFile "table/cell/header")
in do
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
case dbsTemplate of
DBSTCourse c l r s -> do
wRows <- forM rows $ \row' -> let
Course{..} = row' ^. c . _entityVal
lecturerUsers = row' ^. l
courseLecturers = userSurname . entityVal <$> lecturerUsers
isRegistered = row' ^. r
courseSchoolName = schoolName $ row' ^. s . _entityVal
courseSemester = (termToText . unTermKey) courseTerm
in return $(widgetFile "table/course/course-teaser")
return $(widgetFile "table/course/colonnade")
DBSTDefault -> do
wRows <- forM rows $ \row' -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row') -> cell') -> do
widget <- cell' ^. cellContents
let attrs = cell' ^. cellAttrs
return $(widgetFile "table/cell/body")
return $(widgetFile "table/colonnade")
pageCount
| PagesizeLimit l <- psLimit

View File

@ -2,7 +2,8 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Zip
( ZipError(..)
( typeZip, extensionZip
, ZipError(..)
, ZipInfo(..)
, produceZip
, consumeZip
@ -27,6 +28,16 @@ import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd)
import qualified Data.Map as Map
typeZip :: ContentType
typeZip = "application/zip"
extensionZip :: Extension
extensionZip = fromMaybe "zip" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeZip ]
instance Default ZipInfo where
def = ZipInfo
@ -95,7 +106,7 @@ modifyFileTitle f = mapC $ \x@File{..} -> x{ fileTitle = f fileTitle }
-- Takes FileInfo and if it is a ZIP-Archive, extract files, otherwiese yield fileinfo
sourceFiles :: (MonadLogger m, MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
sourceFiles fInfo
| mimeType == "application/zip" = do
| ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP"
fileSource fInfo =$= void consumeZip
| otherwise = do

View File

@ -34,8 +34,11 @@ import Text.Shakespeare.Text (st)
import Control.Monad.Trans.Reader (mapReaderT)
import Control.Monad.Except (MonadError(..))
import Utils (exceptT, allM, whenIsJust, guardM)
import Utils.Lens (_NoUpload)
import Utils.DB (getKeyBy)
import Control.Lens
import Numeric.Natural
import qualified Net.IP as IP
@ -398,6 +401,50 @@ customMigrations = Map.fromListWith (>>)
updateTransactionInfo _ = return ()
runConduit $ getLogEntries .| C.mapM_ updateTransactionInfo
)
, ( AppliedMigrationKey [migrationVersion|16.0.0|] [version|17.0.0|]
, do
whenM (tableExists "allocation_course") $ do
vals <- [sqlQQ| SELECT "course", "instructions", "application_text", "application_files", "ratings_visible" FROM "allocation_course"; |]
whenM (tableExists "course") $ do
[executeQQ|
ALTER TABLE "course" ADD COLUMN "applications_required" boolean not null default #{False}, ADD COLUMN "applications_instructions" varchar null, ADD COLUMN "applications_text" boolean not null default #{False}, ADD COLUMN "applications_files" jsonb not null default #{NoUpload}, ADD COLUMN "applications_ratings_visible" boolean not null default #{False};
ALTER TABLE "course" ALTER COLUMN "applications_required" DROP DEFAULT, ALTER COLUMN "applications_text" DROP DEFAULT, ALTER COLUMN "applications_files" DROP DEFAULT, ALTER COLUMN "applications_ratings_visible" DROP DEFAULT;
|]
forM_ vals $ \(cid :: CourseId, Single applicationsInstructions :: Single (Maybe Html), Single applicationsText :: Single Bool, Single applicationsFiles :: Single UploadMode, Single applicationsRatingsVisible :: Single Bool) -> do
let appRequired = applicationsText || isn't _NoUpload applicationsFiles
[executeQQ|
UPDATE "course" SET ("applications_required", "applications_instructions", "applications_text", "applications_files", "applications_ratings_visible") = (#{appRequired}, #{applicationsInstructions}, #{applicationsText}, #{applicationsFiles}, #{applicationsRatingsVisible}) WHERE "id" = #{cid};
|]
[executeQQ|
ALTER TABLE "allocation_course" DROP COLUMN "instructions", DROP COLUMN "application_text", DROP COLUMN "application_files", DROP COLUMN "ratings_visible";
|]
whenM ((&&) <$> tableExists "allocation_course_file" <*> (not <$> tableExists "course_app_instruction_file")) $ do
[executeQQ|
CREATe TABLE "course_app_instruction_file"("id" SERIAL8 PRIMARY KEY UNIQUE,"course" INT8 NOT NULL,"file" INT8 NOT NULL);
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "unique_course_app_instruction_file" UNIQUE("course","file");
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "course_app_instruction_file_course_fkey" FOREIGN KEY("course") REFERENCES "course"("id");
ALTER TABLE "course_app_instruction_file" ADD CONSTRAINT "course_app_instruction_file_file_fkey" FOREIGN KEY("file") REFERENCES "file"("id");
|]
let getFileEntries = rawQuery [st|SELECT "allocation_course_file"."id", "allocation_course"."course", "allocation_course_file"."file" FROM "allocation_course_file" INNER JOIN "allocation_course" ON "allocation_course"."id" = "allocation_course_file"."allocation_course"|] []
moveFileEntry [fromPersistValue -> Right (acfId :: Int64), fromPersistValue -> Right (cid :: CourseId), fromPersistValue -> Right (fid :: FileId)] =
[executeQQ|
INSERT INTO "course_app_instruction_file" ("course", "file") VALUES (#{cid}, #{fid});
DELETE FROM "allocation_course_file" WHERE "id" = #{acfId};
|]
moveFileEntry _ = return ()
runConduit $ getFileEntries .| C.mapM_ moveFileEntry
tableDropEmpty "allocation_course_file"
whenM (tableExists "allocation_application") $
tableDropEmpty "allocation_application"
whenM (tableExists "allocation_application_file") $
tableDropEmpty "allocation_application_file"
)
]

View File

@ -45,6 +45,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthCourseRegistered
| AuthTutorialRegistered
| AuthExamRegistered
| AuthExamResult
| AuthParticipant
| AuthTime
| AuthAllocationTime

View File

@ -54,6 +54,9 @@ data Icon
| IconSFTSolution -- for SheetFileType only
| IconSFTMarking -- for SheetFileType only
| IconEmail
| IconRegisterTemplate
| IconApplyTrue
| IconApplyFalse
deriving (Eq, Ord, Enum, Bounded, Show, Read)
iconText :: Icon -> Text
@ -82,6 +85,9 @@ iconText = \case
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
instance Universe Icon
instance Finite Icon
@ -150,6 +156,10 @@ iconEnrol :: Bool -> Markup
iconEnrol True = icon IconEnrolTrue
iconEnrol False = icon IconEnrolFalse
iconApply :: Bool -> Markup
iconApply True = icon IconApplyTrue
iconApply False = icon IconApplyFalse
iconExamRegister :: Bool -> Markup
iconExamRegister True = icon IconExamRegisterTrue
iconExamRegister False = icon IconExamRegisterFalse

View File

@ -35,6 +35,9 @@ _InnerJoinRight f (E.InnerJoin l r) = (l `E.InnerJoin`) <$> f r
_nullable :: MonoFoldable mono => Prism' mono (NonNull mono)
_nullable = prism' toNullable fromNullable
_SchoolId :: Iso' SchoolId SchoolShorthand
_SchoolId = iso unSchoolKey SchoolKey
-----------------------------------
-- Lens Definitions for our Types
@ -120,6 +123,7 @@ makePrisms ''HandlerContents
makePrisms ''ErrorResponse
makePrisms ''UploadMode
makeLenses_ ''UploadMode
makeLenses_ ''SubmissionMode

View File

@ -13,6 +13,6 @@
Achtung, dieser Link löscht momentan noch den kompletten Benutzer
unwiderruflich aus der Live-Datenbank mit
<code>DELETE CASCADE uid
\ Klausurdaten müssen jedoch langfristig gespeichert werden!
\ Prüfungs- und Klausurdaten müssen jedoch langfristig gespeichert werden!
<p>
^{modal "Benutzer löschen" (Right deleteWidget)}

View File

@ -79,13 +79,38 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<div>
\ <em>Achtung:</em>
\ Abmeldung nur bis #{dereg} erlaubt.
$maybe aInst <- courseApplicationsInstructions course
<dt .deflist__dt>
$if courseApplicationsRequired course
_{MsgCourseApplicationInstructionsApplication}
$else
_{MsgCourseApplicationInstructionsRegistration}
<dd .deflist__dd>
<div>
#{aInst}
$if hasApplicationTemplate
<p>
<a href=@{CourseR tid ssh csh CRegisterTemplateR}>
#{iconRegisterTemplate} #
$if courseApplicationsRequired course
_{MsgCourseApplicationTemplateApplication}
$else
_{MsgCourseApplicationTemplateRegistration}
$if registrationOpen || isJust mRegAt
<dt .deflist__dt>
_{MsgCourseRegistration}
<dd .deflist__dd>
<div .course__registration>
$if registrationOpen
$# regForm is defined through templates/widgets/registerForm
^{regForm}
$if isJust mApplication && courseApplicationsRequired course
<p>
_{MsgCourseApplicationDeleteToEdit}
$else
$if isJust mRegAt
<p>
_{MsgCourseRegistrationDeleteToEdit}
$maybe date <- mRegAt
_{MsgRegisteredSince} #{date}
<dt .deflist__dt>
@ -106,16 +131,3 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<dd .deflist__dd>
^{tutorialTable}
$# <div .container>
$# <div .tab-group>
$# <div .tab data-tab-name="Übungsblätter">
$# ^{modal "#modal-toggler__new-sheet" Nothing}
$# <h3 .tab-title>Übungsblätter
$# <h1>TODO: Sortierbare Tabelle der bisherigen Übungsblätter
$# <div .tab data-tab-name="Übungsgruppen">
$# <h3 .tab-title>Übungsgruppen
$# <h1>TODO: Sortierbare Tabelle der Übungsgruppen
$# <div .tab data-tab-name="Klausuren">
$# <h3 .tab-title>Klausuren
$# <div>...

View File

@ -0,0 +1,5 @@
$newline never
$if courseApplicationsRequired course
_{MsgCourseLoginToApply}
$else
_{MsgCourseLoginToRegister}

View File

@ -328,6 +328,9 @@ input[type="button"].btn-info:hover,
/* SCROLLTABLE */
.scrolltable {
overflow: auto;
}
.scrolltable--bordered {
box-shadow: 0 0 1px 1px var(--color-grey-light);
}

View File

@ -1,17 +1,8 @@
<p>
<h3>
Momentan noch unimplementierte Funktionalitäten
Neue geplante Funktionalitäten, moment noch nicht implementiert
<ul>
<li>
<h4>
aus UniWorX bekannt:
<ul>
<li> Zentralanmeldungen
<li>
<h4>
neue geplante Features:
<ul>
<li> Stundenplan/Kalender mit allen Veranstaltungen und Klausuren
<li> Vollständige Vorlesungshomepages
<li> Vollständige Internationalisierung deutsch/englisch/...
<li> Stundenplan/Kalender mit allen Veranstaltungen und Klausuren
<li> Vollständige Vorlesungshomepages
<li> Vollständige Internationalisierung deutsch/englisch/...

View File

@ -0,0 +1,32 @@
$newline text
<p>
Uni2Work wird mit Hilfe des
<a href="https://www.yesodweb.com/">
Yesod-Frameworks
\ in
<a href="https://www.haskell.org/">
Haskell
\ /
<a href="https://www.haskell.org/ghc/">
GHC
\ implementiert.
Die Implementierung genügt
<a href="https://de.wikipedia.org/wiki/Representational_State_Transfer">
REST-Prinzipien
, so dass eine gute horizontale Skalierbarkeit der Webapplikation gewährleistet wird.
Als Datenbank wird ein
<a href="https://www.postgresql.org/">
PostgreSQL
\ Server verwendet.
<p>
<h4>
An der Entwicklung von Uni2Work beteiligte Personen
<ul .list--iconless .list--inline .list--comma-separated>
<li>Felix Hamann (Frontend)
<li>Steffen Jost
<li>Gregor Kleen
<li>Sarah Vaupel

View File

@ -7,11 +7,31 @@ $newline text
<h2>Bekannte Probleme in Bearbeitung
<dl .deflist>
<dt .deflist__dt>Klausuren #{iconNew}
<dt .deflist__dt>#{iconNew} Zentralanmeldung
<dd .deflist__dd>
Klausuren werden ab sofort teilweise unterstüzt.
Der genaue Stand der Entwicklung ist weiter unter auf dieser
Seite in einem eigenem Abschnitt detailliert.
<p>
Veranstalter können eigene Veranstaltungen zu verschiedenen Zentralanmeldungen hinzufügen.
Dies findet man unter dem Menupunkt "Kurs editieren"
<p>
Die Zentralanmeldungen selbst sind momentan aber noch nicht sichtbar; dies folgt in Kürze.
<p>
Weitere Details finden sich weiter unter auf dieser Seite in einem
<a href="#allocations">
eigenem Abschnitt
<dt .deflist__dt>Klausuren und Prüfungen
<dd .deflist__dd>
<p>
Klausuren werden bereits teilweise unterstüzt.
Im Gegensatz zu UniWorX werden nun auch allgemeinere Prüfungsformen unterstüzt,
z.B. mündlcihe Prüfungen oder auch Praktika, bei denen die Teilnehmer in kleineren Gruppen
zu verschiedenen Zeitpunkten geprüft werden.
<p>
Der genaue Stand der Entwicklung ist weiter unter auf dieser Seite in einem
<a href="#exams">
eigenem Abschnitt
\ detailliert.
<dt .deflist__dt>Benachrichtigungen
<dd .deflist__dd>
@ -41,10 +61,20 @@ $newline text
Dabei werden vor allem Kurskürzel und die Kursbeschreibung übernommen;
nicht jedoch Übungsblätter, Klausuren oder Anmeldungen.
<pr>
<p>
Die Kursbeschreibung kann in Html verfasst werden und
<em>sollte die Modulbeschreibung enthalten!
<dt .deflist__dt> #{iconNew} Unterstützung für verschiedene Institute
<dd .deflist__dd>
<p>
Uni2work unterstüzt die Verwaltung von mehreren Instituten, d.h.
Kursnamen wie "[MATH]" für Kurse des mathematischen Instituts werden ab sofort nicht mehr benötigt.
Stattdessen gibt es nun Instituts-Filter für Kurslisten.
<p>
Die Berechtigungen der Uni2work-Administratoren sind auf Kurse des jeweiligen Instituts eingeschränkt,
d.h. ein Uni2work Administrator der Informatik kann keine Noten aus Kursen der Mathematik einsehen.
<dt .deflist__dt> Materialzugriff
<dd .deflist__dd>
Der Zugriff auf Übungsblätter, Folien und andere Materialien
@ -248,9 +278,13 @@ $newline text
Um die Anmeldung in beliebig viele Tutoriumsgruppen zuzulassen können alle Registrierungs-Gruppen leer gelassen werden.
<section>
<h2> Klausuren
<p> Das Verwalten von Klausuren und Notenmeldungen wurde nun teilweise implementiert und ist ab sofort einsetzbar.
<section id="exams">
<h2> Klausuren und Prüfungen
<p> Das Verwalten von Klausuren und Prüfungen im Allgemeinen sind bereits teilweise implementiert und einsetzbar.
In Erweiterung zu UniWorX werden nun auch allgemeinere Prüfungsformen unterstüzt,
z.B. mündlcihe Prüfungen oder auch Praktika, bei denen die Teilnehmer in kleineren Gruppen
zu verschiedenen Zeitpunkten und in verschiedenen Räumen geprüft werden.
<dl .deflist>
<dt .deflist__dt> Anlegen/Editieren
<dd .deflist__dd>
@ -267,26 +301,22 @@ $newline text
Es lassen sich aber auch zeitlich getrennte Prüfungen verwalten, wie z.B. mündliche Prüfungen bei Seminaren oder Praktika.
Teilnehmern wird eine übersichtliche Tabelle aller Prüfungen angezeigt.
<dt .deflist__dt> #{iconNew} Anmeldungen
<dd .deflist__dd>
Teilnehmer können sich zu sichtbaren Klausuren innerhalb des eingestellten
Anmeldezeitraums anmelden.
Die Teilnehmerlisten können online oder per CSV Export/Import bearbeitet werden.
<dt .deflist__dt> #{iconProblem} Prüfungszuteilung
<dd .deflist__dd>
Auf Wunsch kann Uni2work die Zuteilung der Teilnehmer auf die Prüfungen (Räume bzw. Prüfungstermine)
nach verschiedenen Kriterien wie Name oder Matrikelnummer vornehmen.
<dt .deflist__dt> #{iconWarning} Anmeldungen
<dd .deflist__dd>
Teilnehmer können sich bereits zu sichtbaren Klausur innerhalb des eingestellten
Anmeldezeitraums anmelden.
<p>
<em>
Achtung: #
Die Liste der angemeldeten Teilnehmer ist momentan noch nicht einsehbar oder exportierbar, wird aber sicher gespeichert.
Dieses Feature folgt in Kürze.
<dt .deflist__dt> #{iconProblem} Korrekturen
<dd .deflist__dd>
<p>
Korrekturen können derzeit noch nicht eingetragen werden.
Die Realisierung sollte in wenigen Wochen erfolgen.
Die Realisierung sollte bald erfolgen.
<p>
Die Eintragung von Korrekturen erfolgt immer pro Teilaufgabe.
Optional kann aus der erreichten Punktesumme dann automatisch eine Gesamtnote berechnet werden.
@ -299,7 +329,12 @@ $newline text
Es werden verschiedene Möglichkeiten angebotenen werden,
die erzielten Bewertungen der Hausübungen
unter einstellbaren Bedingungen
in einen Klausurbonus umzurechnen (z.B. anrechnung nur, falls bereits ohne Bonus bestanden).
in einen Klausurbonus umzurechnen (z.B. Anrechnung nur, falls bereits ohne Bonus bestanden).
<dt .deflist__dt> #{iconProblem} Türschilder
<dd .deflist__dd>
Das Drucken von passenden Türschildern "Bitte Ruhe!" mit den passenden Eckdaten der Klausur
wird momentan noch nicht unterstüzt.
<dt .deflist__dt> #{iconProblem} Notenmeldung
<dd .deflist__dd>
@ -319,6 +354,35 @@ $newline text
Prüfungsamt übermitteln. Für größere Änderungen kann das Prüfungsamt
die Klausur auch wieder an den Dozenten zurück übergeben;
der Dozent trägt dann einfach ein späteres Datum für die Übergabe ein.
<section id="allocations">
<h2> Zentralanmeldungen
<dl .deflist>
<dt .deflist__dt> Namensschema
<dd .deflist__dd>
Veranstaltungen können einen beliebigen Namen tragen.
Die behelfsmäßigen Kürzel wie [SB], [ZP], usw sind nicht mehr notwendig!
<dt .deflist__dt> Kurseinstellung
<dd .deflist__dd>
Die Kurseinstellungen werden ggf. von den notwendigen Einstellungen
der jeweiligen Zentralanmeldung überschrieben, d.h. Veranstalter
können hier keine Fehler mehr machen.
<dt .deflist__dt> Individuelle Bewerbungen
<dd .deflist__dd>
Studierende können nun pro Kurs eine individuelle Bewerbung abgeben,
welche nur den jeweiligen Kursverwaltern zugestelt wird.
<dt .deflist__dt> Feedback zu Bewerbungen
<dd .deflist__dd>
Veranstalter können, wenn sie das möchten, den Bewerbern ein Feedback
zu ihren Bewerbungen geben.
<section>
<h2>Sonstiges
<dl .deflist>

View File

@ -1,8 +1,8 @@
$newline never
<p>
Stand: July 2019
Stand: August 2019
<ul>
<li>
Benachrichtigungen per eMail treffen manchmal mit mehreren Tagen Verzögerung ein.
Zentralanmledungen sind noch nicht für Studierende sichtbar (folgt in Kürze)
<li>
Format von Bewertungsdateien ist noch provisorisch

View File

@ -134,7 +134,7 @@
Benutzerdaten bleiben prinzipiell so lange gespeichert,
bis ein Institutsadministrator über die Exmatrikulation informiert wurde.
Dann wird der Account mit einer angemessenen zeitverzögerung gelöscht.
Anonymisierte Klausurnoten verbleiben aus statistischen Gründen dauerhaft im System.
Anonymisierte Prüfungsnoten verbleiben aus statistischen Gründen dauerhaft im System.
<li>
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.

View File

@ -1,5 +1,5 @@
$newline never
<div .scrolltable>
<div .scrolltable .scrolltable--bordered>
<table *{dbsAttrs'}>
$maybe wHeaders' <- wHeaders
<thead>
@ -17,5 +17,4 @@ $newline never
$forall row <- wRows
<tr .table__row>
$forall widget <- row
$# cell/body.hamlet
^{widget}

View File

@ -0,0 +1,12 @@
$newline never
<div .scrolltable .div__course-teaser *{dbsAttrs'}>
$maybe wHeaders' <- wHeaders
<div .course-teaser-header>
$forall widget <- wHeaders'
^{widget}
$nothing
$if null wRows && (dbsEmptyStyle == DBESHeading)
<p>_{dbsEmptyMessage}
$else
$forall row <- wRows
^{row}

View File

@ -0,0 +1,45 @@
:root {
--color-grey-light: #efefef;
--color-grey-lighter: #f5f5f5;
--color-fontsec: #5b5861;
--course-bg-color: var(--color-grey-lighter);
--course-expanded-bg-color: var(--color-grey-light);
}
.scrolltable {
box-shadow: none!important;
}
.course-header::after,
.course-header::before {
content: '';
position: absolute;
right: 10px;
top: 20px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid rgba(255, 255, 255, 0.4);
}
.course-header::before {
/* magic numbers to move arrow back in the right position after flipping it.
this allows us to use the same border for the up and the down arrow */
transform: translateY(150%) scale(1, -1);
transform-origin: top;
}
.course-header::after {
transform: translateY(-150%);
}
.course-header:hover::before,
.course-header:hover::after {
border-bottom-color: rgba(255, 255, 255, 0.7);
}
.sorted-asc::before,
.sorted-desc::after {
border-bottom-color: white !important;
}

View File

@ -0,0 +1,25 @@
<div uw-course-teaser :isRegistered:.course-teaser__registered :is _Nothing courseDescription:.course-teaser__disabled :is _Just courseDescription:tabindex="1">
<div .course-teaser__semester>
<a href=@{TermCourseListR courseTerm}>_{courseSemester}
<div .course-teaser__school-value>
<a href=@{TermSchoolCourseListR courseTerm courseSchool}>_{courseSchoolName}
<div .course-teaser__shorthand>_{courseShorthand}
<div .course-teaser__title>
<a href=@{CourseR courseTerm courseSchool courseShorthand CShowR}>
_{courseName}
$if isRegistered
<div .course-teaser__registration>_{MsgRegistered}
$if not $ null courseLecturers
<div .course-teaser__lecturer-label>
_{MsgLecturersForN (length courseLecturers)}
<div .course-teaser__lecturer-value>
<ul .list--inline .list--comma-separated>
$forall lecturer <- courseLecturers
<li>
#{lecturer}
$maybe regTo <- courseRegisterTo
<div .course-teaser__duedate-label>_{MsgRegisterTo}
<div .course-teaser__duedate-value>^{formatTimeW SelFormatDateTime regTo}
$maybe desc <- courseDescription
<div .course-teaser__chevron>
<div .course-teaser__description>#{desc}

View File

@ -0,0 +1,12 @@
$newline never
$maybe flag <- sortableKey
<span .course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
$case directions
$of [SortAsc]
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
^{widget}
$of _
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
^{widget}
$nothing

View File

@ -7,6 +7,11 @@ $newline never
_{MsgKnownBugs}
^{knownBugs}
<section>
<h2>
_{MsgImplementationDetails}
^{implementation}
<section>
<h2>
_{MsgVersionHistory}

View File

@ -6,7 +6,7 @@
werden Benutzer hiermit vollständig aus der Live-Datenbank mit
<code>DELETE CASCADE uid
gelöscht.
Klausurdaten müssen jedoch unbedingt 5 Jahre bis nach Exmatrikulation
Klausur und Prüfungsdaten müssen jedoch unbedingt 5 Jahre bis nach Exmatrikulation
aufbewahrt werden!
<p>
Benutzer können sich mit Ihrem Campus-Account

View File

@ -24,8 +24,8 @@ $# --
<th .table__th>_{MsgSheetGradingPoints'}
<th .table__th>_{MsgSheetGradingCount'}
$# Number of Sheet/Submissions used for calculating maximum passes/points
$forall row <- rowWdgts
^{row}
$forall someRow <- rowsShown
^{rowWgt someRow}
$maybe nrNoGrade <- positiveSum $ numNotGraded
<tr .table__row>
<th .table__th>_{MsgSheetTypeNotGraded}

View File

@ -420,6 +420,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing
, courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit jost now ffp
void . insert $ DegreeCourse ffp sdBsc sdInf
@ -452,6 +457,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing
, courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit fhamann now eip
void . insert' $ DegreeCourse eip sdBsc sdInf
@ -470,6 +480,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing
, courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit fhamann now ixd
void . insert' $ DegreeCourse ixd sdBsc sdInf
@ -488,6 +503,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing
, courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit fhamann now ux3
void . insert' $ DegreeCourse ux3 sdBsc sdInf
@ -506,6 +526,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Nothing
, courseMaterialFree = True
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit jost now pmo
void . insert $ DegreeCourse pmo sdBsc sdInf
@ -662,6 +687,11 @@ fillDb = do
, courseDeregisterUntil = Nothing
, courseRegisterSecret = Just "dbs"
, courseMaterialFree = False
, courseApplicationsRequired = False
, courseApplicationsInstructions = Nothing
, courseApplicationsText = False
, courseApplicationsFiles = NoUpload
, courseApplicationsRatingsVisible = False
}
insert_ $ CourseEdit gkleen now dbs
void . insert' $ DegreeCourse dbs sdBsc sdInf

View File

@ -16,7 +16,13 @@ instance Arbitrary (Route Auth) where
]
instance Arbitrary (Route EmbeddedStatic) where
arbitrary = embeddedResourceR <$> arbitrary <*> arbitrary
arbitrary = do
let printableText = pack . filter (/= '/') . getPrintableString <$> arbitrary
pathLength <- getPositive <$> arbitrary
path <- replicateM pathLength printableText
paramNum <- getNonNegative <$> arbitrary
params <- replicateM paramNum $ (,) <$> printableText <*> printableText
return $ embeddedResourceR path params
instance Arbitrary CourseR where
arbitrary = genericArbitrary
@ -42,6 +48,10 @@ instance Arbitrary ExamR where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary CourseApplicationR where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary (Route UniWorX) where
arbitrary = genericArbitrary
shrink = genericShrink