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. 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) ### [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; this._asyncTableHeader = this._element.dataset.asyncTableDbHeader;
} }
const table = this._element.querySelector('table'); const table = this._element.querySelector('table, .div__course-teaser');
if (!table) { 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; const rawTableId = table.id;
@ -94,7 +94,7 @@ export class AsyncTable {
} }
_setupSortableHeaders() { _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 })); .map((th) => ({ element: th }));
this._ths.forEach((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 { MassInput } from './mass-input/mass-input';
import { Modal } from './modal/modal'; import { Modal } from './modal/modal';
import { Tooltip } from './tooltips/tooltips'; import { Tooltip } from './tooltips/tooltips';
import { CourseTeaser } from './course-teaser/course-teaser';
export const Utils = [ export const Utils = [
Alerts, Alerts,
@ -23,4 +24,5 @@ export const Utils = [
Modal, Modal,
ShowHide, ShowHide,
Tooltip, Tooltip,
CourseTeaser,
]; ];

View File

@ -7,8 +7,10 @@ BtnRegister: Anmelden
BtnDeregister: Abmelden BtnDeregister: Abmelden
BtnCourseRegister: Zum Kurs anmelden BtnCourseRegister: Zum Kurs anmelden
BtnCourseDeregister: Vom Kurs abmelden BtnCourseDeregister: Vom Kurs abmelden
BtnExamRegister: Anmelden zur Klausur BtnCourseApply: Zum Kurs bewerben
BtnExamDeregister: Von der Klausur abmelden BtnCourseRetractApplication: Bewerbung zum Kurs zurückziehen
BtnExamRegister: Anmelden zur Prüfung
BtnExamDeregister: Von der Prüfung abmelden
BtnHijack: Sitzung übernehmen BtnHijack: Sitzung übernehmen
BtnSave: Speichern BtnSave: Speichern
PressSaveToSave: Änderungen werden erst durch Drücken des Knopfes "Speichern" gespeichert. 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. CourseNoCapacity: In diesem Kurs sind keine Plätze mehr frei.
TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei. TutorialNoCapacity: In dieser Übung sind keine Plätze mehr frei.
CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet. CourseNotEmpty: In diesem Kurs sind momentan Teilnehmer angemeldet.
CourseRegisterOk: Anmeldung erfolgreich CourseRegistration: Kursanmeldung
CourseDeregisterOk: Erfolgreich abgemeldet 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 CourseDeregisterLecturerTip: Wenn Sie den Teilnehmer vom Kurs abmelden kann es sein, dass sie Zugriff auf diese Daten verlieren
CourseStudyFeature: Assoziiertes Hauptfach CourseStudyFeature: Assoziiertes Studienfach
CourseStudyFeatureUpdated: Assoziiertes Hauptfach geändert CourseStudyFeatureTip: Dient ausschließlich der Information der Kursverwalter
CourseStudyFeatureNone: Kein assoziiertes Hauptfach CourseStudyFeatureUpdated: Assoziiertes Studienfach geändert
CourseStudyFeatureNone: Kein assoziiertes Studienfach
CourseTutorial: Tutorium CourseTutorial: Tutorium
CourseStudyFeatureTooltip: Korrekte Angabe kann Notenweiterleitungen beschleunigen CourseSecretWrong: Falsches Passwort
CourseSecretWrong: Falsches Kennwort
CourseSecret: Zugangspasswort CourseSecret: Zugangspasswort
CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{tid}-#{ssh}-#{csh} wurde erfolgreich geändert. 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. 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 CourseMemberOf: Teilnehmer
CourseMembersCount n@Int: #{n} CourseMembersCount n@Int: #{n}
CourseMembersCountLimited n@Int max@Int: #{n}/#{max} 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 CourseName: Name
CourseDescription: Beschreibung CourseDescription: Beschreibung
CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet CourseDescriptionTip: Beliebiges HTML-Markup ist gestattet
@ -136,8 +142,8 @@ CourseUserNote: Notiz
CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar CourseUserNoteTooltip: Nur für Dozenten dieses Kurses einsehbar
CourseUserNoteSaved: Notizänderungen gespeichert CourseUserNoteSaved: Notizänderungen gespeichert
CourseUserNoteDeleted: Teilnehmernotiz gelöscht CourseUserNoteDeleted: Teilnehmernotiz gelöscht
CourseUserDeregister: Abmelden CourseUserDeregister: Vom Kurs abmelden
CourseUsersDeregistered count@Int64: #{show count} Teilnehmer abgemeldet CourseUsersDeregistered count@Int64: #{show count} Teilnehmer vom Kurs abgemeldet
CourseUserSendMail: Mitteilung verschicken CourseUserSendMail: Mitteilung verschicken
TutorialUserDeregister: Vom Tutorium Abmelden TutorialUserDeregister: Vom Tutorium Abmelden
TutorialUserSendMail: Mitteilung verschicken TutorialUserSendMail: Mitteilung verschicken
@ -149,20 +155,48 @@ CourseAllocationOption term@Text name@Text: #{name} (#{term})
CourseAllocationMinCapacity: Minimale Teilnehmeranzahl 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 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 CourseAllocationMinCapacityMustBeNonNegative: Minimale Teilnehmeranzahl darf nicht negativ sein
CourseAllocationInstructions: Anweisungen zur Bewerbung CourseApplicationInstructions: Anweisungen zur Bewerbung/Anmeldung
CourseAllocationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben CourseApplicationInstructionsTip: Wird den Studierenden angezeigt, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseAllocationApplicationTemplate: Bewerbungsvorlagen CourseApplicationTemplate: Bewerbungsvorlagen
CourseAllocationApplicationText: Text-Bewerbungen CourseApplicationTemplateTip: Werden den Studierenden zum download angeboten, wenn diese sich für Ihre Veranstaltung bewerben bzw. bei dieser anmelden
CourseAllocationApplicationTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen? CourseApplicationsText: Text-Bewerbungen
CourseAllocationApplicationRatingsVisible: Feedback für Bewerbungen CourseApplicationsTextTip: Sollen die Studierenden Bewerbungen (ggf. zusätzlich zu abgegebenen Dateien) als unformatierten Text einreichen?
CourseAllocationApplicationRatingsVisibleTip: Sollen Bewertung und Kommentar der Bewerbungen den Studierenden nach Ende der Bewertungs-Phase angezeigt werden? 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 CourseNoAllocationsAvailable: Es sind aktuell keine Zentralanmeldungen verfügbar
AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden AllocationStaffRegisterToExpired: Es dürfen keine Änderungen an der Eintragung des Kurses zur Zentralanmeldung mehr vorgenommen werden
CourseFormSectionRegistration: Anmeldung CourseFormSectionRegistration: Anmeldung zum Kurs
CourseFormSectionAdministration: Verwaltung CourseFormSectionAdministration: Verwaltung
CourseLecturers: Kursverwalter CourseLecturers: Kursverwalter
@ -183,6 +217,8 @@ NoSuchSchool ssh@SchoolId: Institut #{ssh} gibt es nicht.
NoSuchCourseShorthand csh@CourseShorthand: Kein Kurs mit Kürzel #{csh} bekannt. NoSuchCourseShorthand csh@CourseShorthand: Kein Kurs mit Kürzel #{csh} bekannt.
NoSuchCourse: Keinen passenden Kurs gefunden. NoSuchCourse: Keinen passenden Kurs gefunden.
NoCourseDescription: Zu diesem Kurs ist keine Beschreibung verfügbar.
Sheet: Blatt Sheet: Blatt
SheetList tid@TermId ssh@SchoolId csh@CourseShorthand: #{tid}-#{ssh}-#{csh} Übersicht Übungsblätter 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 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"} MaterialDelHasFiles count@Int64: inklusive #{count} #{pluralDE count "Datei" "Dateien"}
MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht. MaterialIsVisible: Achtung, dieses Material wurde bereits veröffentlicht.
MaterialDeleted materialName@MaterialName: Material "#{materialName}" gelöscht 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. Unauthorized: Sie haben hierfür keine explizite Berechtigung.
UnauthorizedAnd l@Text r@Text: (#{l} UND #{r}) UnauthorizedAnd l@Text r@Text: (#{l} UND #{r})
UnauthorizedOr l@Text r@Text: (#{l} ODER #{r}) UnauthorizedOr l@Text r@Text: (#{l} ODER #{r})
UnauthorizedNot i@Text: (NICHT #{i})
UnauthorizedNoToken: Ihrer Anfrage war kein Authorisierungs-Token beigefügt. UnauthorizedNoToken: Ihrer Anfrage war kein Authorisierungs-Token beigefügt.
UnauthorizedTokenExpired: Ihr Authorisierungs-Token ist abgelaufen. UnauthorizedTokenExpired: Ihr Authorisierungs-Token ist abgelaufen.
UnauthorizedTokenNotStarted: Ihr Authorisierungs-Token ist noch nicht gültig. 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. UnauthorizedSheetCorrector: Sie sind nicht als Korrektor für dieses Übungsblatt eingetragen.
UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen. UnauthorizedCorrectorAny: Sie sind nicht als Korrektor für eine Veranstaltung eingetragen.
UnauthorizedRegistered: Sie sind nicht als Teilnehmer für diese Veranstaltung registriert. 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. UnauthorizedParticipant: Angegebener Benutzer ist nicht als Teilnehmer dieser Veranstaltung registriert.
UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen. UnauthorizedCourseTime: Dieses Kurs erlaubt momentan keine Anmeldungen.
UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben. UnauthorizedSheetTime: Dieses Übungsblatt ist momentan nicht freigegeben.
UnauthorizedApplicationTime: Diese Bewerbung ist momentan nicht freigegeben.
UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben. UnauthorizedMaterialTime: Dieses Material ist momentan nicht freigegeben.
UnauthorizedTutorialTime: Dieses Tutorium erlaubt momentan keine Anmeldungen. 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. UnauthorizedSubmissionOwner: Sie sind an dieser Abgabe nicht beteiligt.
UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert. UnauthorizedSubmissionRated: Diese Abgabe ist noch nicht korrigiert.
UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe. UnauthorizedSubmissionCorrector: Sie sind nicht der Korrektor für diese Abgabe.
@ -395,7 +435,7 @@ TokensResetSuccess: Authorisierungs-Tokens invalidiert
HomeOpenCourses: Kurse mit offener Registrierung HomeOpenCourses: Kurse mit offener Registrierung
HomeUpcomingSheets: Anstehende Übungsblätter HomeUpcomingSheets: Anstehende Übungsblätter
HomeUpcomingExams: Bevorstehende Klausuren HomeUpcomingExams: Bevorstehende Prüfungen
NumCourses num@Int64: #{num} Kurse NumCourses num@Int64: #{num} Kurse
CloseAlert: Schliessen 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: CouldNotAssignCorrectorsAuto num@Int64: #{num} Abgaben konnten nicht automatisch zugewiesen werden:
SelfCorrectors num@Int64: #{num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! 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 CorrectionSheets: Übersicht Korrekturen nach Blättern
CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren
@ -532,7 +576,7 @@ MultiSinkException name@Text error@Text: In Abgabe #{name} ist ein Fehler aufget
NoTableContent: Kein Tabelleninhalt NoTableContent: Kein Tabelleninhalt
NoUpcomingSheetDeadlines: Keine anstehenden Übungsblätter 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 AdminHeading: Administration
AdminUserHeading: Benutzeradministration AdminUserHeading: Benutzeradministration
@ -550,6 +594,8 @@ ForSchools n@Int: für #{pluralDE n "Institut" "Institute"}
UserListTitle: Komprehensive Benutzerliste UserListTitle: Komprehensive Benutzerliste
AccessRightsSaved: Berechtigungsänderungen wurden gespeichert. AccessRightsSaved: Berechtigungsänderungen wurden gespeichert.
LecturersForN n@Int: #{pluralDE n "Dozent" "Dozenten"}
Date: Datum Date: Datum
DateTimeFormat: Datums- und Uhrzeitformat DateTimeFormat: Datums- und Uhrzeitformat
DateFormat: Datumsformat DateFormat: Datumsformat
@ -642,7 +688,7 @@ StudyFeatureAge: Fachsemester
StudyFeatureDegree: Abschluss StudyFeatureDegree: Abschluss
FieldPrimary: Hauptfach FieldPrimary: Hauptfach
FieldSecondary: Nebenfach FieldSecondary: Nebenfach
NoPrimaryStudyField: (kein Hauptfach registriert) NoStudyField: Kein Studienfach
StudyFeatureType: StudyFeatureType:
StudyFeatureValid: Aktiv StudyFeatureValid: Aktiv
StudyFeatureUpdate: Abgeglichen StudyFeatureUpdate: Abgeglichen
@ -770,6 +816,9 @@ SheetGroupMaxGroupsize: Maximale Gruppengröße
SheetFiles: Übungsblatt-Dateien SheetFiles: Übungsblatt-Dateien
SheetFileTypeHeader: Zugehörigkeit 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 NotificationTriggerSubmissionRatedGraded: Meine Abgabe in einem gewerteten Übungsblatt wurde korrigiert
NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert
NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen 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 NotificationTriggerCorrectionsNotDistributed: Nicht alle Abgaben eines meiner Übungsblätter konnten einem Korrektor zugeteilt werden
NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert NotificationTriggerUserRightsUpdate: Meine Berechtigungen wurden geändert
NotificationTriggerUserAuthModeUpdate: Mein Anmelde-Modus wurde 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 NotificationTriggerKindAll: Für alle Benutzer
NotificationTriggerKindCourseParticipant: Für Kursteilnehmer NotificationTriggerKindCourseParticipant: Für Kursteilnehmer
NotificationTriggerKindExamParticipant: Für Klausurteilnehmer NotificationTriggerKindExamParticipant: Für Prüfungsteilnehmer
NotificationTriggerKindCorrector: Für Korrektoren NotificationTriggerKindCorrector: Für Korrektoren
NotificationTriggerKindLecturer: Für Dozenten NotificationTriggerKindLecturer: Für Dozenten
NotificationTriggerKindAdmin: Für Administratoren NotificationTriggerKindAdmin: Für Administratoren
@ -945,11 +994,11 @@ MenuAuthPreds: Authorisierungseinstellungen
MenuTutorialDelete: Tutorium löschen MenuTutorialDelete: Tutorium löschen
MenuTutorialEdit: Tutorium editieren MenuTutorialEdit: Tutorium editieren
MenuTutorialComm: Mitteilung an Teilnehmer MenuTutorialComm: Mitteilung an Teilnehmer
MenuExamList: Klausuren MenuExamList: Prüfungen
MenuExamNew: Neue Klausur anlegen MenuExamNew: Neue Prüfung anlegen
MenuExamEdit: Bearbeiten MenuExamEdit: Bearbeiten
MenuExamUsers: Teilnehmer MenuExamUsers: Teilnehmer
MenuExamAddMembers: Klausurteilnehmer hinzufügen MenuExamAddMembers: Prüfungsteilnehmer hinzufügen
MenuLecturerInvite: Dozenten 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. 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 AuthTagAllocationTime: Zeitliche Einschränkungen durch Zentralanmeldung sind erfüllt
AuthTagCourseRegistered: Nutzer ist Kursteilnehmer AuthTagCourseRegistered: Nutzer ist Kursteilnehmer
AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer AuthTagTutorialRegistered: Nutzer ist Tutoriumsteilnehmer
AuthTagExamRegistered: Nutzer ist Klausurteilnehmer AuthTagExamRegistered: Nutzer ist Prüfungsteilnehmer
AuthTagExamResult: Nutzer hat Prüfungsergebnisse
AuthTagParticipant: Nutzer ist mit Kurs assoziiert AuthTagParticipant: Nutzer ist mit Kurs assoziiert
AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe AuthTagRegisterGroup: Nutzer ist nicht Mitglied eines anderen Tutoriums mit der selben Registrierungs-Gruppe
AuthTagCapacity: Kapazität ist ausreichend 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 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 ExamCorrectorInvitationDeclined examn@ExamName: Sie haben die Einladung, Korrektor für #{examn} zu werden, abgelehnt
ExamCorrectorInviteHeading examn@ExamName: Einladung zum Korrektor für #{examn} 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 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 ExamRegistrationInvitationDeclined examn@ExamName: Sie haben die Einladung, Teilnehmer für #{examn} zu werden, abgelehnt
ExamRegistrationInviteHeading examn@ExamName: Einladung zum Teilnehmer für #{examn} 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 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 SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
@ -1102,7 +1152,7 @@ TutorialsHeading: Tutorien
TutorialEdit: Bearbeiten TutorialEdit: Bearbeiten
TutorialDelete: Löschen TutorialDelete: Löschen
CourseExams: Klausuren CourseExams: Prüfungen
CourseTutorials: Übungen CourseTutorials: Übungen
ParticipantsN n@Int: #{n} Teilnehmer 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 CourseParticipants n@Int: Derzeit #{n} angemeldete Kursteilnehmer
CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt CourseParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
CourseParticipantsAlreadyRegistered n@Int: #{n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet 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 CourseParticipantsRegistered n@Int: #{n} Teilnehmer erfolgreich angemeldet
CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen CourseParticipantsRegisterHeading: Kursteilnehmer hinzufügen
ExamRegistrationAndCourseParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} sowohl zum Kurs, als auch 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 Klausur angemeldet, da #{pluralDE n "er" "sie"} nicht zum Kurs angemeldet #{pluralDE n "ist" "sind"} 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 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"} 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 Klausur angemeldet ExamRegistrationParticipantsRegistered n@Int: #{n} Teilnehmer #{pluralDE n "wurde" "wurden"} zur Prüfung angemeldet
ExamRegistrationInviteDeadline: Einladung nur gültig bis ExamRegistrationInviteDeadline: Einladung nur gültig bis
ExamRegistrationEnlistDirectly: Bekannte Nutzer sofort als Teilnehmer eintragen 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 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 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 ExamParticipantsInvited n@Int: #{n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
ExamName: Name ExamName: Name
ExamTime: Termin ExamTime: Termin
ExamsHeading: Klausuren ExamsHeading: Prüfungen
ExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein ExamNameTip: Muss innerhalb der Veranstaltung eindeutig sein
ExamStart: Beginn ExamStart: Beginn
ExamEnd: Ende ExamEnd: Ende
@ -1173,7 +1223,7 @@ ExamDescription: Beschreibung
ExamVisibleFrom: Sichtbar ab ExamVisibleFrom: Sichtbar ab
ExamVisibleFromTip: Ohne Datum nie sichtbar und keine Anmeldung möglich ExamVisibleFromTip: Ohne Datum nie sichtbar und keine Anmeldung möglich
ExamRegisterFrom: Anmeldung ab 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 ExamRegisterTo: Anmeldung bis
ExamDeregisterUntil: Abmeldung bis ExamDeregisterUntil: Abmeldung bis
ExamPublishOccurrenceAssignments: Termin- bzw. Raumzuteilung den Teilnehmern mitteilen um 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 ExamPublishOccurrenceAssignmentsParticipant: Termin- bzw. Raumzuteilung einsehbar ab
ExamFinished: Bewertung abgeschlossen ab ExamFinished: Bewertung abgeschlossen ab
ExamFinishedParticipant: Bewertung vorrausichtlich abgeschlossen 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 ExamClosed: Noten stehen fest ab
ExamClosedTip: Zeitpunkt ab dem keine Änderungen an den Ergebnissen zulässig sind; Prüfungsämter bekommen Einsicht ExamClosedTip: Zeitpunkt ab dem keine Änderungen an den Ergebnissen zulässig sind; Prüfungsämter bekommen Einsicht
ExamShowGrades: Noten anzeigen ExamShowGrades: Noten anzeigen
@ -1197,15 +1247,15 @@ Points: Punkte
PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein PointsMustBeNonNegative: Punktegrenzen dürfen nicht negativ sein
PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein PointsMustBeMonotonic: Punktegrenzen müssen aufsteigend sein
GradingFrom: Ab GradingFrom: Ab
ExamNew: Neue Klausur ExamNew: Neue Prüfung
ExamBonusRule: Klausurbonus aus Übungsbetrieb ExamBonusRule: Prüfungsbonus aus Übungsbetrieb
ExamNoBonus': Kein automatischer Bonus ExamNoBonus': Kein automatischer Bonus
ExamBonusPoints': Umrechnung von Übungspunkten ExamBonusPoints': Umrechnung von Übungspunkten
ExamEditHeading examn@ExamName: #{examn} bearbeiten ExamEditHeading examn@ExamName: #{examn} bearbeiten
ExamBonusMaxPoints: Maximal erreichbare Klausur-Bonuspunkte ExamBonusMaxPoints: Maximal erreichbare Prüfungs-Bonuspunkte
ExamBonusMaxPointsNonPositive: Maximaler Klausurbonus muss positiv und größer null sein ExamBonusMaxPointsNonPositive: Maximaler Prüfungsbonus muss positiv und größer null sein
ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen ExamBonusOnlyPassed: Bonus nur nach Bestehen anrechnen
ExamOccurrenceRule: Automatische Termin- bzw. Raumzuteilung ExamOccurrenceRule: Automatische Termin- bzw. Raumzuteilung
@ -1238,7 +1288,7 @@ ExamFormCorrection: Korrektur
ExamFormParts: Teile ExamFormParts: Teile
ExamCorrectors: Korrektoren 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 ExamParts: Teilaufgaben
ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein ExamPartWeightNegative: Gewicht aller Teilaufgaben muss größer oder gleich Null sein
@ -1248,30 +1298,30 @@ ExamPartMaxPoints: Maximalpunktzahl
ExamPartWeight: Gewichtung ExamPartWeight: Gewichtung
ExamPartResultPoints: Erreichte Punkte 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 ExamCreated exam@ExamName: #{exam} erfolgreich angelegt
ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet ExamEdited exam@ExamName: #{exam} erfolgreich bearbeitet
ExamNoShow: Nicht erschienen ExamNoShow: Nicht erschienen
ExamVoided: Entwertet ExamVoided: Entwertet
ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Klausurpunkte ExamBonusPoints possible@Points: Maximal #{showFixed True possible} Prüfungspunkte
ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Klausurpunkte, falls die Klausur auch ohne Bonus bereits bestanden ist ExamBonusPointsPassed possible@Points: Maximal #{showFixed True possible} Prüfungspunkte, falls die Prüfung auch ohne Bonus bereits bestanden ist
ExamPassed: Bestanden ExamPassed: Bestanden
ExamNotPassed: Nicht bestanden ExamNotPassed: Nicht bestanden
ExamResult: Klausurergebnis ExamResult: Prüfungsergebnis
ExamRegisteredSuccess exam@ExamName: Erfolgreich zur #{exam} angemeldet ExamRegisteredSuccess exam@ExamName: Erfolgreich zur Prüfung #{exam} angemeldet
ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der #{exam} abgemeldet ExamDeregisteredSuccess exam@ExamName: Erfolgreich von der Prüfung #{exam} abgemeldet
ExamRegistered: Angemeldet ExamRegistered: Zur Prüfung angemeldet
ExamNotRegistered: Nicht angemeldet ExamNotRegistered: Nicht zur Prüfung angemeldet
ExamRegistration: Anmeldung ExamRegistration: Prüfungsanmeldung
ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen ExamRegisterToMustBeAfterRegisterFrom: "Anmeldung ab" muss vor "Anmeldung bis" liegen
ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen ExamDeregisterUntilMustBeAfterRegisterFrom: "Abmeldung bis" muss nach "Anmeldung bis" liegen
ExamStartMustBeAfterPublishOccurrenceAssignments: Start muss nach Veröffentlichung der Termin- bzw. Raumzuordnung 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 ExamFinishedMustBeAfterEnd: "Bewertung abgeschlossen ab" muss nach Ende liegen
ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen ExamFinishedMustBeAfterStart: "Bewertung abgeschlossen ab" muss nach Start liegen
ExamClosedMustBeAfterFinished: "Noten stehen fest ab" muss nach "Bewertung abgeschlossen ab" 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 ExamClosedMustBeAfterEnd: "Noten stehen fest ab" muss nach Ende liegen
ExamOccurrenceEndMustBeAfterStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss vor seinem 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 ExamOccurrenceStartMustBeAfterExamStart eoName@ExamOccurrenceName: Beginn des Termins #{eoName} muss nach Beginn der Prüfung liegen
ExamOccurrenceEndMustBeBeforeExamEnd eoName@ExamOccurrenceName: Ende des Termins #{eoName} muss vor Ende der Klausur 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 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 ExamOccurrenceDuplicateName eoName@ExamOccurrenceName: Interne Terminbezeichnung #{eoName} kommt mehrfach vor
VersionHistory: Versionsgeschichte VersionHistory: Versionsgeschichte
KnownBugs: Bekannte Bugs KnownBugs: Bekannte Bugs
ImplementationDetails: Implementierung
ExamUsersHeading: Klausurteilnehmer ExamUsersHeading: Prüfungsteilnehmer
ExamUserDeregister: Teilnehmer von Klausur abmelden ExamUserDeregister: Teilnehmer von Prüfung abmelden
ExamUserAssignOccurrence: Termin/Raum zuweisen 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 ExamUsersOccurrenceUpdated count@Int64: Termin/Raum für #{show count} Teilnehmer gesetzt
CsvFile: CSV-Datei CsvFile: CSV-Datei
@ -1316,15 +1367,15 @@ CsvColumnExamUserSurname: Nachname(n) des Teilnehmers
CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers CsvColumnExamUserFirstName: Vorname(n) des Teilnehmers
CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n)) CsvColumnExamUserName: Voller Name des Teilnehmers (gewöhnlicherweise inkl. Vor- und Nachname(n))
CsvColumnExamUserMatriculation: Matrikelnummer des Teilnehmers CsvColumnExamUserMatriculation: Matrikelnummer des Teilnehmers
CsvColumnExamUserField: Hauptfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat CsvColumnExamUserField: Studienfach, mit dem der Teilnehmer seine Kursanmeldung assoziiert hat
CsvColumnExamUserDegree: Abschluss, den der Teilnehmer im assoziierten Hauptfach anstrebt CsvColumnExamUserDegree: Abschluss, den der Teilnehmer im assoziierten Studienfach anstrebt
CsvColumnExamUserSemester: Fachsemester des Teilnehmers im assoziierten Hauptfach CsvColumnExamUserSemester: Fachsemester des Teilnehmers im assoziierten Studienfach
CsvColumnExamUserOccurrence: Prüfungstermin/-Raum, zu dem der Teilnehmer angemeldet ist CsvColumnExamUserOccurrence: Prüfungstermin/-Raum, zu dem der Teilnehmer angemeldet ist
CsvColumnExamUserExercisePoints: Anzahl von Punkten, die der Teilnehmer im Übungsbetrieb erreicht hat 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 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 CsvColumnExamUserExercisePassesMax: Maximale Anzahl von Übungsblättern, die der Teilnehmer bis zu seinem Prüfungstermin 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") 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 CsvColumnExamUserCourseNote: Notizen zum Teilnehmer
Action: Aktion 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. 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. DBCsvException: Bei der Berechnung der auszuführenden Aktionen für einen Datensatz ist ein Fehler aufgetreten.
ExamUserCsvCourseRegister: Benutzer zum Kurs und zur Klausur anmelden ExamUserCsvCourseRegister: Benutzer zum Kurs und zur Prüfung anmelden
ExamUserCsvRegister: Kursteilnehmer zur Klausur anmelden ExamUserCsvRegister: Kursteilnehmer zur Prüfung anmelden
ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen ExamUserCsvAssignOccurrence: Teilnehmern einen anderen Termin/Raum zuweisen
ExamUserCsvDeregister: Teilnehmer von der Klausur abmelden ExamUserCsvDeregister: Teilnehmer von der Prüfung abmelden
ExamUserCsvSetCourseField: Kurs-assoziiertes Hauptfach ändern ExamUserCsvSetCourseField: Kurs-assoziiertes Studienfach ändern
ExamUserCsvSetResult: Ergebnis eintragen ExamUserCsvSetResult: Ergebnis eintragen
ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen ExamUserCsvSetCourseNote: Teilnehmer-Notizen anpassen
ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht ExamUserCsvCourseNoteDeleted: Notiz wird gelöscht
ExamUserCsvExceptionNoMatchingUser: Kursteilnehmer konnte nicht eindeutig identifiziert werden 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 ExamUserCsvExceptionNoMatchingOccurrence: Raum/Termin konnte nicht eindeutig identifiziert werden
TableHeadingFilter: Filter TableHeadingFilter: Filter
@ -1355,7 +1406,7 @@ TableHeadingCsvExport: CSV-Export
ExamResultAttended: Teilgenommen ExamResultAttended: Teilgenommen
ExamResultNoShow: Nicht erschienen ExamResultNoShow: Nicht erschienen
ExamResultVoided: Entwertet ExamResultVoided: Entwertet
ExamResultNone: Kein Klausurergebnis ExamResultNone: Kein Prüfungsergebnis
BtnAuthLDAP: Auf Campus-Kennung umstellen BtnAuthLDAP: Auf Campus-Kennung umstellen
BtnAuthPWHash: Auf Uni2work-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 AuthLDAPLookupFailed: Nutzer konnte aufgrund eines LDAP-Fehlers nicht nachgeschlagen werden
AuthLDAPInvalidLookup: Bestehender Nutzer konnte nicht eindeutig einem LDAP-Eintrag zugeordnet werden AuthLDAPInvalidLookup: Bestehender Nutzer konnte nicht eindeutig einem LDAP-Eintrag zugeordnet werden
AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung an AuthLDAPAlreadyConfigured: Nutzer meldet sich bereits per Campus-Kennung in Uni2work an
AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung an AuthLDAPConfigured: Nutzer meldet sich nun per Campus-Kennung in Uni2work an
AuthPWHashAlreadyConfigured: Nutzer meldet sich bereits per Uni2work-Kennung an AuthPWHashAlreadyConfigured: Nutzer meldet sich bereits per Uni2work-Kennung an
AuthPWHashConfigured: Nutzer meldet sich nun per Uni2work-Kennung an AuthPWHashConfigured: Nutzer meldet sich nun per Uni2work-Kennung an

View File

@ -31,38 +31,14 @@ AllocationCourse
allocation AllocationId allocation AllocationId
course CourseId course CourseId
minCapacity Int -- if the course would get assigned fewer than this many applicants, restart the assignment process without the course 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 UniqueAllocationCourse course
AllocationCourseFile
allocationCourse AllocationCourseId
file FileId
UniqueAllocationCourseFile allocationCourse file
AllocationUser AllocationUser
allocation AllocationId allocation AllocationId
user UserId user UserId
totalCourses Natural -- number of total allocated courses for this user must be <= than this number totalCourses Natural -- number of total allocated courses for this user must be <= than this number
UniqueAllocationUser allocation user 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 AllocationDeregister -- self-inflicted user-deregistrations from an allocated course
user UserId user UserId
allocation AllocationId Maybe 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 deregisterUntil UTCTime Maybe -- unenrolement may be prohibited from a given date onwards
registerSecret Text Maybe -- enrolement maybe protected by a simple common passphrase 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 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 TermSchoolCourseShort term school shorthand -- shorthand must be unique within school and semester
TermSchoolCourseName term school name -- name must be unique within school and semester TermSchoolCourseName term school name -- name must be unique within school and semester
deriving Generic 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) CourseEdit -- who edited when a row in table "Course", kept indefinitely (might be replaced by generic Audit Table; like all ...-Edit tables)
user UserId user UserId
time UTCTime time UTCTime
@ -59,3 +70,18 @@ CourseUserNoteEdit -- who edited a participants course note when
user UserId user UserId
time UTCTime time UTCTime
note CourseUserNoteId -- PROBLEM: deleted notes have no modification date any more 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", "name": "uni2work",
"version": "4.13.1", "version": "5.0.2",
"description": "", "description": "",
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -1,5 +1,5 @@
name: uniworx name: uniworx
version: 4.13.1 version: 5.0.2
dependencies: dependencies:
# Due to a bug in GHC 8.0.1, we block its usage # 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/new CourseNewR GET POST !lecturer
/course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: /course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer:
/ CShowR GET !free / 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 /edit CEditR GET POST
/lecturer-invite CLecInviteR GET POST /lecturer-invite CLecInviteR GET POST
/delete CDeleteR GET POST !lecturerANDemptyANDallocation-time /delete CDeleteR GET POST !lecturerANDemptyANDallocation-time
@ -152,7 +153,9 @@
/users EUsersR GET POST /users EUsersR GET POST
/users/new EAddUserR GET POST /users/new EAddUserR GET POST
/users/invite EInviteR 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 CorrectionsR GET POST !corrector !lecturer
/subs/upload CorrectionsUploadR GET POST !corrector !lecturer /subs/upload CorrectionsUploadR GET POST !corrector !lecturer

View File

@ -30,6 +30,24 @@ data Transaction
{ transactionExam :: ExamId { transactionExam :: ExamId
, transactionUser :: UserId , 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) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions 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 (ToJSON(..), ToJSONKey(..), ToJSONKeyFunction(..), FromJSON(..), FromJSONKey(..), FromJSONKeyFunction(..), Value(..), withText)
import Data.Aeson.Encoding (text) import Data.Aeson.Encoding (text)
import Text.Blaze (ToMarkup(..))
instance {-# OVERLAPPING #-} MonadThrow m => MonadCrypto (ReaderT CryptoIDKey m) where instance {-# OVERLAPPING #-} MonadThrow m => MonadCrypto (ReaderT CryptoIDKey m) where
type MonadCryptoKey (ReaderT CryptoIDKey m) = CryptoIDKey type MonadCryptoKey (ReaderT CryptoIDKey m) = CryptoIDKey
@ -46,19 +48,23 @@ decCryptoIDs [ ''SubmissionId
, ''ExamOccurrenceId , ''ExamOccurrenceId
, ''ExamPartId , ''ExamPartId
, ''AllocationId , ''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 fromPathPiece (Text.unpack -> piece) = do
piece' <- (stripPrefix `on` map CI.mk) "uwa" piece piece' <- (stripPrefix `on` map CI.mk) "uwa" piece
return . CryptoID . CI.mk $ map CI.original piece' return . CryptoID . CI.mk $ map CI.original piece'
toPathPiece = Text.pack . ("uwa" <>) . CI.foldedCase . ciphertext 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 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) 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 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 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 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 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 Data.Conduit.List (sourceList)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Control.Monad.Except (MonadError(..), ExceptT) import Control.Monad.Except (MonadError(..), ExceptT)
import Control.Monad.Trans.Maybe (MaybeT(..)) import Control.Monad.Trans.Maybe (MaybeT(..))
@ -150,6 +151,7 @@ deriving instance Generic SubmissionR
deriving instance Generic MaterialR deriving instance Generic MaterialR
deriving instance Generic TutorialR deriving instance Generic TutorialR
deriving instance Generic ExamR deriving instance Generic ExamR
deriving instance Generic CourseApplicationR
deriving instance Generic (Route UniWorX) deriving instance Generic (Route UniWorX)
-- | Convenient Type Synonyms: -- | Convenient Type Synonyms:
@ -179,6 +181,10 @@ pattern CSubmissionR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Cr
pattern CSubmissionR tid ssh csh shn cid ptn pattern CSubmissionR tid ssh csh shn cid ptn
= CSheetR tid ssh csh shn (SubmissionR 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) pluralDE :: (Eq a, Num a)
=> a -- ^ Count => a -- ^ Count
@ -282,6 +288,7 @@ embedRenderMessage ''UniWorX ''MessageStatus ("Message" <>)
embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel
embedRenderMessage ''UniWorX ''StudyFieldType id embedRenderMessage ''UniWorX ''StudyFieldType id
embedRenderMessage ''UniWorX ''SheetFileType id embedRenderMessage ''UniWorX ''SheetFileType id
embedRenderMessage ''UniWorX ''SubmissionFileType id
embedRenderMessage ''UniWorX ''CorrectorState id embedRenderMessage ''UniWorX ''CorrectorState id
embedRenderMessage ''UniWorX ''RatingException id embedRenderMessage ''UniWorX ''RatingException id
embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>) embedRenderMessage ''UniWorX ''SubmissionSinkException ("SubmissionSinkException" <>)
@ -518,6 +525,11 @@ andAR _ _ reason@(Unauthorized _) = reason
andAR _ Authorized other = other andAR _ Authorized other = other
andAR _ AuthenticationRequired _ = AuthenticationRequired 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, falseAR :: MsgRendererS UniWorX -> AuthResult
trueAR = const Authorized trueAR = const Authorized
falseAR = Unauthorized . ($ MsgUnauthorized) . render falseAR = Unauthorized . ($ MsgUnauthorized) . render
@ -580,14 +592,13 @@ tagAccessPredicate AuthAdmin = APDB $ \mAuthId route _ -> case route of
-- Courses: access only to school admins -- Courses: access only to school admins
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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.on $ course E.^. CourseSchool E.==. userAdmin E.^. UserAdminSchool
E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val authId E.where_ $ userAdmin E.^. UserAdminUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT isAdmin (unauthorizedI MsgUnauthorizedSchoolAdmin)
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedSchoolAdmin)
return Authorized return Authorized
-- other routes: access to any admin is granted here -- other routes: access to any admin is granted here
_other -> $cachedHereBinary mAuthId . exceptT return return $ do _other -> $cachedHereBinary mAuthId . exceptT return return $ do
@ -622,14 +633,13 @@ tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthLecturer = APDB $ \mAuthId route _ -> case route of
CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do CourseR tid ssh csh _ -> $cachedHereBinary (mAuthId, tid, ssh, csh) . exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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.on $ course E.^. CourseId E.==. lecturer E.^. LecturerCourse
E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId E.where_ $ lecturer E.^. LecturerUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT isLecturer (unauthorizedI MsgUnauthorizedLecturer)
guardMExceptT (c>0) (unauthorizedI MsgUnauthorizedLecturer)
return Authorized return Authorized
-- lecturer for any school will do -- lecturer for any school will do
_ -> $cachedHereBinary mAuthId . exceptT return return $ 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) guardMExceptT (not $ Map.null resMap) (unauthorizedI MsgUnauthorizedTutor)
return Authorized return Authorized
tagAccessPredicate AuthTime = APDB $ \mAuthId route _ -> case route of 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 CExamR tid ssh csh examn subRoute -> maybeT (unauthorizedI MsgUnauthorizedExamTime) $ do
course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh course <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ TermSchoolCourseShort tid ssh csh
Entity eId Exam{..} <- $cachedHereBinary (course, examn) . MaybeT . getBy $ UniqueExam course examn 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 | not registered
, maybe False (now >=) courseRegisterFrom -- Nothing => no registration allowed , maybe False (now >=) courseRegisterFrom -- Nothing => no registration allowed
, maybe True (now <=) courseRegisterTo -> return Authorized , maybe True (now <=) courseRegisterTo -> return Authorized
(Just (Entity _ Course{courseDeregisterUntil})) (Just (Entity cid Course{courseDeregisterUntil}))
| registered | 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 _other -> unauthorizedI MsgUnauthorizedCourseTime
MessageR cID -> maybeT (unauthorizedI MsgUnauthorizedSystemMessageTime) $ do 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 tagAccessPredicate AuthCourseRegistered = APDB $ \mAuthId route _ -> case route of
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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.on $ course E.^. CourseId E.==. courseParticipant E.^. CourseParticipantCourse
E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId E.where_ $ courseParticipant E.^. CourseParticipantUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCourseRegistered r r -> $unsupportedAuthPredicate AuthCourseRegistered r
tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthTutorialRegistered = APDB $ \mAuthId route _ -> case route of
CTutorialR tid ssh csh tutn _ -> exceptT return return $ do CTutorialR tid ssh csh tutn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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 $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId 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.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. tutorial E.^. TutorialName E.==. E.val tutn E.&&. tutorial E.^. TutorialName E.==. E.val tutn
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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 $ tutorial E.^. TutorialId E.==. tutorialParticipant E.^. TutorialParticipantTutorial
E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse E.on $ course E.^. CourseId E.==. tutorial E.^. TutorialCourse
E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId E.where_ $ tutorialParticipant E.^. TutorialParticipantUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT isRegistered (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthTutorialRegistered r r -> $unsupportedAuthPredicate AuthTutorialRegistered r
tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of tagAccessPredicate AuthExamRegistered = APDB $ \mAuthId route _ -> case route of
CExamR tid ssh csh examn _ -> exceptT return return $ do CExamR tid ssh csh examn _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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 $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId 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.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
E.&&. exam E.^. ExamName E.==. E.val examn E.&&. exam E.^. ExamName E.==. E.val examn
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT (c > 0) (unauthorizedI MsgUnauthorizedRegistered)
return Authorized return Authorized
CourseR tid ssh csh _ -> exceptT return return $ do CourseR tid ssh csh _ -> exceptT return return $ do
authId <- maybeExceptT AuthenticationRequired $ return mAuthId 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 $ exam E.^. ExamId E.==. examRegistration E.^. ExamRegistrationExam
E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse E.on $ course E.^. CourseId E.==. exam E.^. ExamCourse
E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId E.where_ $ examRegistration E.^. ExamRegistrationUser E.==. E.val authId
E.&&. course E.^. CourseTerm E.==. E.val tid E.&&. course E.^. CourseTerm E.==. E.val tid
E.&&. course E.^. CourseSchool E.==. E.val ssh E.&&. course E.^. CourseSchool E.==. E.val ssh
E.&&. course E.^. CourseShorthand E.==. E.val csh E.&&. course E.^. CourseShorthand E.==. E.val csh
return (E.countRows :: E.SqlExpr (E.Value Int64)) guardMExceptT hasRegistration (unauthorizedI MsgUnauthorizedRegistered)
guardMExceptT (c > 0) (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 return Authorized
r -> $unsupportedAuthPredicate AuthExamRegistered r r -> $unsupportedAuthPredicate AuthExamRegistered r
tagAccessPredicate AuthParticipant = APDB $ \_ route _ -> case route of 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 -- Entity cid Course{..} <- MaybeT . getBy $ TermSchoolCourseShort tid ssh csh
cid <- $cachedHereBinary (tid, ssh, csh) . MaybeT . getKeyBy $ 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) . $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.on $ sheet E.^. SheetId E.==. submission E.^. SubmissionSheet
E.where_ $ sheet E.^. SheetCourse E.==. E.val cid E.where_ $ sheet E.^. SheetCourse E.==. E.val cid
return E.countRows
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthEmpty r r -> $unsupportedAuthPredicate AuthEmpty r
tagAccessPredicate AuthMaterials = APDB $ \_ route _ -> case route of tagAccessPredicate AuthMaterials = APDB $ \_ route _ -> case route of
@ -1044,19 +1104,24 @@ tagAccessPredicate AuthCorrectorSubmissions = APDB $ \_ route _ -> case route of
guard submissionModeCorrector guard submissionModeCorrector
return Authorized return Authorized
r -> $unsupportedAuthPredicate AuthCorrectorSubmissions r 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 referencedUser <- case route of
AdminUserR cID -> return cID AdminUserR cID -> decrypt cID
AdminUserDeleteR cID -> return cID AdminUserDeleteR cID -> decrypt cID
AdminHijackUserR cID -> return cID AdminHijackUserR cID -> decrypt cID
UserNotificationR cID -> return cID UserNotificationR cID -> decrypt cID
UserPasswordR cID -> return cID UserPasswordR cID -> decrypt cID
CourseR _ _ _ (CUserR cID) -> return 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 _other -> throwError =<< $unsupportedAuthPredicate AuthSelf route
referencedUser' <- decrypt referencedUser
case mAuthId of case mAuthId of
Just uid Just uid
| uid == referencedUser' -> return Authorized | uid == referencedUser -> return Authorized
Nothing -> return AuthenticationRequired Nothing -> return AuthenticationRequired
_other -> unauthorizedI MsgUnauthorizedSelf _other -> unauthorizedI MsgUnauthorizedSelf
tagAccessPredicate AuthIsLDAP = APDB $ \_ route _ -> exceptT return return $ do 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 :: AuthLiteral -> WriterT (Set AuthTag) m AuthResult
evalAuthLiteral PLVariable{..} = evalAuthTag plVar evalAuthLiteral PLVariable{..} = evalAuthTag plVar
evalAuthLiteral PLNegated{..} = evalAuthTag plVar >>= \case evalAuthLiteral PLNegated{..} = notAR mr plVar <$> evalAuthTag plVar
Unauthorized _ -> return Authorized
AuthenticationRequired -> return AuthenticationRequired
Authorized -> unauthorizedI plVar
orAR', andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult orAR', andAR' :: forall m'. Monad m' => m' AuthResult -> m' AuthResult -> m' AuthResult
orAR' = shortCircuitM (is _Authorized) (orAR mr) 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 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 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 (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 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 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 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 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) breadcrumb (CTutorialR tid ssh csh tutn TEditR) = return ("Bearbeiten", Just $ CTutorialR tid ssh csh tutn TUsersR)
@ -2731,6 +2793,7 @@ routeNormalizers =
, ncCourse , ncCourse
, ncSheet , ncSheet
, verifySubmission , verifySubmission
, verifyCourseApplication
] ]
where where
normalizeRender route = route <$ do normalizeRender route = route <$ do
@ -2777,6 +2840,14 @@ routeNormalizers =
let newRoute = CSubmissionR courseTerm courseSchool courseShorthand sheetName cID sr let newRoute = CSubmissionR courseTerm courseSchool courseShorthand sheetName cID sr
tell . Any $ route /= newRoute tell . Any $ route /= newRoute
return 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. -- 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.Show as Handler.Course
import Handler.Course.User as Handler.Course import Handler.Course.User as Handler.Course
import Handler.Course.Users as Handler.Course import Handler.Course.Users as Handler.Course
import Handler.Course.Application as Handler.Course
getCHiWisR :: TermId -> SchoolId -> CourseShorthand -> Handler Html 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 , cfLink :: Maybe Text
, cfMatFree :: Bool , cfMatFree :: Bool
, cfAllocation :: Maybe AllocationCourseForm , cfAllocation :: Maybe AllocationCourseForm
, cfAppRequired :: Bool
, cfAppInstructions :: Maybe Html
, cfAppInstructionFiles :: Maybe (Source Handler (Either FileId File))
, cfAppText :: Bool
, cfAppFiles :: UploadMode
, cfAppRatingsVisible :: Bool
, cfCapacity :: Maybe Int , cfCapacity :: Maybe Int
, cfSecret :: Maybe Text , cfSecret :: Maybe Text
, cfRegFrom :: Maybe UTCTime , cfRegFrom :: Maybe UTCTime
@ -51,43 +57,45 @@ data CourseForm = CourseForm
data AllocationCourseForm = AllocationCourseForm data AllocationCourseForm = AllocationCourseForm
{ acfAllocation :: AllocationId { acfAllocation :: AllocationId
, acfInstructions :: Maybe Html
, acfFiles :: Maybe (Source Handler (Either FileId File))
, acfApplicationText :: Bool
, acfApplicationFiles :: UploadMode
, acfApplicationRatingsVisible :: Bool
, acfMinCapacity :: Int , acfMinCapacity :: Int
} }
courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm courseToForm :: Entity Course -> [Lecturer] -> [(UserEmail, InvitationDBData Lecturer)] -> Maybe (Entity AllocationCourse) -> CourseForm
courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm courseToForm (Entity cid Course{..}) lecs lecInvites alloc = CourseForm
{ cfCourseId = Just cid { cfCourseId = Just cid
, cfName = courseName , cfName = courseName
, cfDesc = courseDescription , cfDesc = courseDescription
, cfLink = courseLinkExternal , cfLink = courseLinkExternal
, cfShort = courseShorthand , cfShort = courseShorthand
, cfTerm = courseTerm , cfTerm = courseTerm
, cfSchool = courseSchool , cfSchool = courseSchool
, cfCapacity = courseCapacity , cfCapacity = courseCapacity
, cfSecret = courseRegisterSecret , cfSecret = courseRegisterSecret
, cfMatFree = courseMaterialFree , cfMatFree = courseMaterialFree
, cfRegFrom = courseRegisterFrom , cfAllocation = allocationCourseToForm <$> alloc
, cfRegTo = courseRegisterTo , cfAppRequired = courseApplicationsRequired
, cfDeRegUntil = courseDeregisterUntil , cfAppInstructions = courseApplicationsInstructions
, cfAllocation = allocationCourseToForm <$> alloc , cfAppInstructionFiles
, cfLecturers = [Right (lecturerUser, lecturerType) | Lecturer{..} <- lecs] , cfAppText = courseApplicationsText
++ [Left (email, mType) | (email, InvDBDataLecturer mType) <- lecInvites ] , 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
allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm allocationCourseToForm (Entity _ AllocationCourse{..}) = AllocationCourseForm
{ acfAllocation = allocationCourseAllocation { acfAllocation = allocationCourseAllocation
, acfMinCapacity = allocationCourseMinCapacity , 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 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 _ -> do
allocationOptions <- mkOptionList <$> mapM mkAllocationOption availableAllocations 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 let
allocationForm' = AllocationCourseForm allocationForm' = AllocationCourseForm
<$> apreq (selectField' Nothing $ return allocationOptions) (fslI MsgCourseAllocation) (fmap acfAllocation $ template >>= cfAllocation) <$> 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) <*> apreq (natFieldI MsgCourseAllocationMinCapacityMustBeNonNegative) (fslI MsgCourseAllocationMinCapacity & setTooltip MsgCourseAllocationMinCapacityTip) (fmap acfMinCapacity $ template >>= cfAllocation)
optionalActionW allocationForm' (fslI MsgCourseAllocationParticipate & setTooltip MsgCourseAllocationParticipateTip) (is _Just . cfAllocation <$> template) 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) <*> apopt checkBoxField (fslI MsgMaterialFree) (cfMatFree <$> template)
<* aformSection MsgCourseFormSectionRegistration <* aformSection MsgCourseFormSectionRegistration
<*> allocationForm <*> 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 <*> aopt (natFieldI MsgCourseCapacity) (fslI MsgCourseCapacity
& setTooltip MsgCourseCapacityTip) (cfCapacity <$> template) & setTooltip MsgCourseCapacityTip) (cfCapacity <$> template)
<*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat) <*> aopt textField (fslpI MsgCourseSecret (mr MsgCourseSecretFormat)
@ -425,20 +427,26 @@ courseEditHandler miButtonAction mbCourseForm = do
} -> do -- create new course } -> do -- create new course
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
insertOkay <- runDBJobs $ do insertOkay <- runDBJobs $ do
insertOkay <- insertUnique Course insertOkay <- let CourseForm{..} = res
{ courseName = cfName res in insertUnique Course
, courseDescription = cfDesc res { courseName = cfName
, courseLinkExternal = cfLink res , courseDescription = cfDesc
, courseShorthand = cfShort res , courseLinkExternal = cfLink
, courseTerm = cfTerm res , courseShorthand = cfShort
, courseSchool = cfSchool res , courseTerm = cfTerm
, courseCapacity = cfCapacity res , courseSchool = cfSchool
, courseRegisterSecret = cfSecret res , courseCapacity = cfCapacity
, courseMaterialFree = cfMatFree res , courseRegisterSecret = cfSecret
, courseRegisterFrom = cfRegFrom res , courseMaterialFree = cfMatFree
, courseRegisterTo = cfRegTo res , courseApplicationsRequired = cfAppRequired
, courseDeregisterUntil = cfDeRegUntil res , courseApplicationsInstructions = cfAppInstructions
} , courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
whenIsJust insertOkay $ \cid -> do whenIsJust insertOkay $ \cid -> do
let (invites, adds) = partitionEithers $ cfLecturers res let (invites, adds) = partitionEithers $ cfLecturers res
insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds insertMany_ $ map (\(lid, lty) -> Lecturer lid cid lty) adds
@ -466,20 +474,26 @@ courseEditHandler miButtonAction mbCourseForm = do
case old of case old of
Nothing -> addMessageI Error MsgInvalidInput $> False Nothing -> addMessageI Error MsgInvalidInput $> False
(Just _) -> do (Just _) -> do
updOkay <- myReplaceUnique cid Course updOkay <- let CourseForm{..} = res
{ courseName = cfName res in myReplaceUnique cid Course
, courseDescription = cfDesc res { courseName = cfName
, courseLinkExternal = cfLink res , courseDescription = cfDesc
, courseShorthand = cfShort res , courseLinkExternal = cfLink
, courseTerm = cfTerm res -- dangerous , courseShorthand = cfShort
, courseSchool = cfSchool res , courseTerm = cfTerm -- dangerous
, courseCapacity = cfCapacity res , courseSchool = cfSchool
, courseRegisterSecret = cfSecret res , courseCapacity = cfCapacity
, courseMaterialFree = cfMatFree res , courseRegisterSecret = cfSecret
, courseRegisterFrom = cfRegFrom res , courseMaterialFree = cfMatFree
, courseRegisterTo = cfRegTo res , courseApplicationsRequired = cfAppRequired
, courseDeregisterUntil = cfDeRegUntil res , courseApplicationsInstructions = cfAppInstructions
} , courseApplicationsText = cfAppText
, courseApplicationsFiles = cfAppFiles
, courseApplicationsRatingsVisible = cfAppRatingsVisible
, courseRegisterFrom = cfRegFrom
, courseRegisterTo = cfRegTo
, courseDeregisterUntil = cfDeRegUntil
}
case updOkay of case updOkay of
(Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False (Just _) -> addMessageI Warning (MsgCourseEditDupShort tid ssh csh) $> False
Nothing -> do Nothing -> do
@ -490,7 +504,19 @@ courseEditHandler miButtonAction mbCourseForm = do
sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites sinkInvitationsF lecturerInvitationConfig $ map (\(lEmail, mLty) -> (lEmail, cid, (InvDBDataLecturer mLty, InvTokenDataLecturer))) invites
insert_ $ CourseEdit aid now cid 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 upsertAllocationCourse cid $ cfAllocation res
addMessageI Success $ MsgCourseEditOk tid ssh csh addMessageI Success $ MsgCourseEditOk tid ssh csh
return True return True
when success $ redirect $ CourseR tid ssh csh CShowR when success $ redirect $ CourseR tid ssh csh CShowR
@ -522,38 +548,17 @@ upsertAllocationCourse cid cfAllocation = do
when doEdit $ when doEdit $
case cfAllocation of case cfAllocation of
Just AllocationCourseForm{..} -> do Just AllocationCourseForm{..} ->
Entity acId _ <- upsert AllocationCourse void $ upsert AllocationCourse
{ allocationCourseAllocation = acfAllocation { allocationCourseAllocation = acfAllocation
, allocationCourseCourse = cid , allocationCourseCourse = cid
, allocationCourseMinCapacity = acfMinCapacity , allocationCourseMinCapacity = acfMinCapacity
, allocationCourseInstructions = acfInstructions
, allocationCourseApplicationText = acfApplicationText
, allocationCourseApplicationFiles = acfApplicationFiles
, allocationCourseRatingsVisible = acfApplicationRatingsVisible
} }
[ AllocationCourseAllocation =. acfAllocation [ AllocationCourseAllocation =. acfAllocation
, AllocationCourseCourse =. cid , AllocationCourseCourse =. cid
, AllocationCourseMinCapacity =. acfMinCapacity , AllocationCourseMinCapacity =. acfMinCapacity
, AllocationCourseInstructions =. acfInstructions
, AllocationCourseApplicationText =. acfApplicationText
, AllocationCourseApplicationFiles =. acfApplicationFiles
, AllocationCourseRatingsVisible =. acfApplicationRatingsVisible
] ]
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 Nothing
| Just (Entity prevId _) <- prevAllocationCourse | Just (Entity prevId _) <- prevAllocationCourse
-> do -> delete prevId
acfs <- selectList [ AllocationCourseFileAllocationCourse ==. prevId ] []
mapM_ deleteCascade $ map (allocationCourseFileFile . entityVal) acfs
delete prevId
_other -> return () _other -> return ()

View File

@ -8,6 +8,8 @@ module Handler.Course.List
import Import import Import
import Data.Maybe (fromJust)
import Utils.Lens import Utils.Lens
import Utils.Form import Utils.Form
-- import Utils.DB -- import Utils.DB
@ -20,83 +22,43 @@ import qualified Data.Set as Set
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Database.Esqueleto as E 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. -- 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 :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCourse = sortable (Just "course") (i18nCell MsgCourse) colCourse = sortable (Just "course") (i18nCell MsgCourse)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR)
[whamlet|_{courseName}|] [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 :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colDescription = sortable Nothing mempty colDescription = sortable Nothing mempty
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
case courseDescription of case courseDescription of
Nothing -> mempty Nothing -> mempty
(Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr) (Just descr) -> cell $ modal (toWidget $ hasComment True) (Right $ toWidget descr)
colCShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a) colCShort :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort) colCShort = sortable (Just "cshort") (i18nCell MsgCourseShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (CourseR courseTerm courseSchool courseShorthand CShowR) [whamlet|_{courseShorthand}|] 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 :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colTerm = sortable (Just "term") (i18nCell MsgTerm) colTerm = sortable (Just "term") (i18nCell MsgTerm)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, _, _) } ->
anchorCell (TermCourseListR courseTerm) [whamlet|#{courseTerm}|] 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 :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort) colSchoolShort = sortable (Just "schoolshort") (i18nCell MsgCourseSchoolShort)
$ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}) } -> $ \DBRow{ dbrOutput=(Entity _ Course{..}, _, _, Entity _ School{..}, _) } ->
anchorCell (TermSchoolCourseListR courseTerm courseSchool) [whamlet|_{schoolShorthand}|] 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 :: IsDBTable m a => Colonnade Sortable CourseTableData (DBCell m a)
colRegistered = sortable (Just "registered") (i18nCell MsgRegistered) 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) 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 => _ -> Colonnade h CourseTableData (DBCell m x) -> PSValidator m x -> DB Widget
makeCourseTable whereClause colChoices psValidator = do makeCourseTable whereClause colChoices psValidator = do
muid <- lift maybeAuthId muid <- lift maybeAuthId
now <- liftIO getCurrentTime
let dbtSQLQuery :: CourseTableExpr -> E.SqlQuery _ let dbtSQLQuery :: CourseTableExpr -> E.SqlQuery _
dbtSQLQuery qin@(course `E.InnerJoin` school) = do dbtSQLQuery qin@(course `E.InnerJoin` school) = do
E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId E.on $ course E.^. CourseSchool E.==. school E.^. SchoolId
@ -121,8 +84,14 @@ makeCourseTable whereClause colChoices psValidator = do
let registered = course2Registered muid qin let registered = course2Registered muid qin
E.where_ $ whereClause (course, participants, registered) E.where_ $ whereClause (course, participants, registered)
return (course, participants, registered, school) 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 :: 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 snd <$> dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
, dbtRowKey = \(course `E.InnerJoin` _) -> course E.^. CourseId , 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) | Set.null criterias -> E.val True :: E.SqlExpr (E.Value Bool)
| otherwise -> school E.^. SchoolShorthand `E.in_` E.valList (Set.toList criterias) | 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 , ( "registered", FilterColumn $ \tExpr criterion -> case getLast (criterion :: Last Bool) of
Nothing -> E.val True :: E.SqlExpr (E.Value Bool) Nothing -> E.val True :: E.SqlExpr (E.Value Bool)
Just needle -> course2Registered muid tExpr E.==. E.val needle Just needle -> course2Registered muid tExpr E.==. E.val needle
@ -175,10 +156,18 @@ makeCourseTable whereClause colChoices psValidator = do
) )
] ]
, dbtFilterUI = \mPrev -> mconcat $ catMaybes , 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)) , 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 , dbtParams = def
, dbtIdent = "courses" :: Text , dbtIdent = "courses" :: Text
, dbtCsvEncode = noCsvEncode , dbtCsvEncode = noCsvEncode
@ -208,53 +197,13 @@ getTermCurrentR :: Handler Html
getTermCurrentR = do getTermCurrentR = do
termIds <- runDB $ selectKeysList [TermActive ==. True] [Desc TermName] termIds <- runDB $ selectKeysList [TermActive ==. True] [Desc TermName]
case fromNullable termIds of case fromNullable termIds of
Nothing -> notFound Nothing
(Just (maximum -> tid)) -> -> notFound
redirect $ TermCourseListR tid -- redirect avoids problematic breadcrumbs, headings, etc. Just (maximum -> tid)
-> redirect (CourseListR, [("courses-term", toPathPiece tid)]) -- redirect avoids problematic breadcrumbs, headings, etc.
getTermSchoolCourseListR :: TermId -> SchoolId -> Handler Html getTermSchoolCourseListR :: TermId -> SchoolId -> Handler Html
getTermSchoolCourseListR tid ssh = do getTermSchoolCourseListR tid ssh = redirect (CourseListR, [("courses-term", toPathPiece tid), ("courses-schoolshort", toPathPiece ssh)])
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")
getTermCourseListR :: TermId -> Handler Html getTermCourseListR :: TermId -> Handler Html
getTermCourseListR tid = do getTermCourseListR tid = redirect (CourseListR, [("courses-term", toPathPiece tid)])
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")

View File

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

View File

@ -1,66 +1,155 @@
module Handler.Course.Register module Handler.Course.Register
( ButtonCourseRegister(..) ( ButtonCourseRegister(..)
, CourseRegisterForm(..)
, courseRegisterForm , courseRegisterForm
, getCRegisterR, postCRegisterR , getCRegisterR, postCRegisterR
) where ) where
import Import import Import
import Utils.Form import Utils.Lens
import Handler.Utils import Handler.Utils
import Data.Function ((&)) 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 -- Dedicated CourseRegistrationButton
data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister data ButtonCourseRegister = BtnCourseRegister | BtnCourseDeregister | BtnCourseApply | BtnCourseRetractApplication
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
instance Universe ButtonCourseRegister instance Universe ButtonCourseRegister
instance Finite ButtonCourseRegister instance Finite ButtonCourseRegister
nullaryPathPiece ''ButtonCourseRegister $ camelToPathPiece' 1 nullaryPathPiece ''ButtonCourseRegister $ camelToPathPiece' 1
embedRenderMessage ''UniWorX ''ButtonCourseRegister id embedRenderMessage ''UniWorX ''ButtonCourseRegister id
instance Button UniWorX ButtonCourseRegister where instance Button UniWorX ButtonCourseRegister where
btnClasses BtnCourseRegister = [BCIsButton, BCPrimary] btnClasses BtnCourseRegister = [BCIsButton, BCPrimary]
btnClasses BtnCourseDeregister = [BCIsButton, BCDanger] btnClasses BtnCourseDeregister = [BCIsButton, BCDanger]
btnClasses BtnCourseApply = [BCIsButton, BCPrimary]
btnClasses BtnCourseRetractApplication = [BCIsButton, BCDanger]
btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|] btnLabel BtnCourseRegister = [whamlet|#{iconEnrol True} _{MsgBtnCourseRegister}|]
btnLabel BtnCourseDeregister = [whamlet|#{iconEnrol False} _{MsgBtnCourseDeregister}|] 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. -- | Workaround for klicking register button without being logged in.
-- After log in, the user sees a "get request not supported" error. -- After log in, the user sees a "get request not supported" error.
getCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -76,21 +165,87 @@ getCRegisterR tid ssh csh = do
postCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html postCRegisterR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
postCRegisterR tid ssh csh = do postCRegisterR tid ssh csh = do
aid <- requireAuthId uid <- requireAuthId
(cid, course, registration) <- runDB $ do course@(Entity cid Course{..}) <- runDB . getBy404 $ TermSchoolCourseShort tid ssh csh
(Entity cid course) <- getBy404 $ TermSchoolCourseShort tid ssh csh (courseRegisterForm', courseRegisterButton) <- courseRegisterForm course
registration <- getBy (UniqueParticipant aid cid) ((regResult,_), _) <- runFormPost $ renderAForm FormStandard courseRegisterForm'
return (cid, course, entityVal <$> registration) formResult regResult $ \CourseRegisterForm{..} -> do
let isRegistered = isJust registration cTime <- liftIO getCurrentTime
((regResult,_), _) <- runFormPost $ courseRegisterForm (Just aid) registration Nothing $ courseRegisterSecret course let
formResult regResult $ \(mbSfId,codeOk) -> if mkApplication
| isRegistered -> do | courseApplicationsRequired || is _Just (void crfApplicationText <|> void crfApplicationFiles)
runDB $ deleteBy $ UniqueParticipant aid cid = void <$> do
addMessageIconI Info IconEnrolFalse MsgCourseDeregisterOk appIds <- selectKeysList [ CourseApplicationAllocation ==. Nothing, CourseApplicationCourse ==. cid, CourseApplicationUser ==. uid ] []
| codeOk -> do appRes <- case appIds of
actTime <- liftIO getCurrentTime [] -> insertUnique $ CourseApplication cid uid crfStudyFeatures crfApplicationText Nothing Nothing Nothing Nothing cTime
regOk <- runDB $ insertUnique $ CourseParticipant cid aid actTime mbSfId False (prevId:ps) -> do
when (isJust regOk) $ addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk forM_ ps $ \appId -> do
| otherwise -> addMessageI Warning MsgCourseSecretWrong deleteApplicationFiles appId
-- addMessage Info $ toHtml $ show regResult -- For debugging only 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 redirect $ CourseR tid ssh csh CShowR

View File

@ -1,5 +1,6 @@
module Handler.Course.Show module Handler.Course.Show
( getCShowR ( getCShowR
, getCRegisterTemplateR
) where ) where
import Import import Import
@ -11,7 +12,7 @@ import qualified Database.Esqueleto.Utils as E
import Database.Esqueleto.Utils.TH import Database.Esqueleto.Utils.TH
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Data.Function ((&)) import Utils.Lens
import qualified Data.Map as Map import qualified Data.Map as Map
@ -19,11 +20,15 @@ import qualified Database.Esqueleto as E
import Handler.Course.Register import Handler.Course.Register
import System.FilePath (addExtension)
import qualified Data.Conduit.List as C
getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCShowR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
getCShowR tid ssh csh = do getCShowR tid ssh csh = do
mbAid <- maybeAuthId 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)] [(E.Entity cid course, E.Value schoolName, E.Value participants, fmap entityVal -> registration)]
<- lift . E.select . E.from $ <- lift . E.select . E.from $
\((school `E.InnerJoin` course) `E.LeftOuterJoin` participant) -> do \((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 E.where_ $ part E.^. CourseParticipantCourse E.==. course E.^. CourseId
return ( E.countRows :: E.SqlExpr (E.Value Int)) return ( E.countRows :: E.SqlExpr (E.Value Int))
return (course,school E.^. SchoolName, numParticipants, participant) 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 staff <- lift . E.select $ E.from $ \(lecturer `E.InnerJoin` user) -> do
E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId E.on $ lecturer E.^. LecturerUser E.==. user E.^. UserId
E.where_ $ lecturer E.^. LecturerCourse E.==. E.val cid 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.where_ $ allocationCourse E.^. AllocationCourseCourse E.==. E.val cid
E.limit 1 E.limit 1
return allocation 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 mRegFrom <- traverse (formatTime SelFormatDateTime) $ courseRegisterFrom course
mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course mRegTo <- traverse (formatTime SelFormatDateTime) $ courseRegisterTo course
mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course mDereg <- traverse (formatTime SelFormatDateTime) $ courseDeregisterUntil course
mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration mRegAt <- traverse (formatTime SelFormatDateTime) $ courseParticipantRegistration <$> registration
(regWidget, regEnctype) <- generateFormPost $ courseRegisterForm mbAid registration defSFid $ courseRegisterSecret course regForm <- if
let regForm = wrapForm regWidget def | is _Just mbAid -> do
{ formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR (courseRegisterForm', regButton) <- courseRegisterForm (Entity cid course)
, formEncoding = regEnctype (regWidget, regEnctype) <- generateFormPost $ renderAForm FormStandard courseRegisterForm'
, formSubmit = FormNoSubmit return $ wrapForm' regButton regWidget def
} { formAction = Just . SomeRoute $ CourseR tid ssh csh CRegisterR
registrationOpen <- (==Authorized) <$> isAuthorized (CourseR tid ssh csh CRegisterR) True , formEncoding = regEnctype
, formSubmit = FormSubmit
}
| otherwise
-> return . modal $(widgetFile "course/login-to-register") . Left . SomeRoute $ AuthR LoginR
registrationOpen <- hasWriteAccessTo $ CourseR tid ssh csh CRegisterR
let let
tutorialDBTable = DBTable{..} tutorialDBTable = DBTable{..}
@ -224,3 +236,15 @@ getCShowR tid ssh csh = do
siteLayout (toWgt $ courseName course) $ do siteLayout (toWgt $ courseName course) $ do
setTitleI $ prependCourseTitle tid ssh csh (""::Text) setTitleI $ prependCourseTitle tid ssh csh (""::Text)
$(widgetFile "course") $(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 -> ((regFieldRes, regFieldView), regFieldEnctype) <- runFormPost . identifyForm FIDcRegField $ \csrf ->
let currentField :: Maybe (Maybe StudyFeaturesId) let currentField :: Maybe (Maybe StudyFeaturesId)
currentField = courseParticipantField . entityVal <$> mRegistration 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 let registrationFieldFrag :: Text
registrationFieldFrag = "registration-field" registrationFieldFrag = "registration-field"
@ -111,6 +111,7 @@ postCUserR tid ssh csh uCId = do
formResult regFieldRes $ \courseParticipantField' -> do formResult regFieldRes $ \courseParticipantField' -> do
runDB $ do runDB $ do
update pId [ CourseParticipantField =. courseParticipantField' ] update pId [ CourseParticipantField =. courseParticipantField' ]
audit $ TransactionCourseParticipantEdit cid uid
addMessageI Success MsgCourseStudyFeatureUpdated addMessageI Success MsgCourseStudyFeatureUpdated
redirect $ currentRoute :#: registrationFieldFrag redirect $ currentRoute :#: registrationFieldFrag
@ -140,17 +141,22 @@ postCUserR tid ssh csh uCId = do
-> invalidArgs ["User not registered"] -> invalidArgs ["User not registered"]
BtnCourseRegister -> do BtnCourseRegister -> do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let primaryField let field
| [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesType == FieldPrimary && studyFeaturesValid) studies | [(Entity featId _, _, _)] <- filter (\(Entity _ StudyFeatures{..}, _, _) -> studyFeaturesValid) studies
= Just featId = Just featId
| otherwise | otherwise
= Nothing = 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 case pId of
Just _ -> do Just _ -> do
addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk addMessageIconI Success IconEnrolTrue MsgCourseRegisterOk
redirect currentRoute redirect currentRoute
Nothing -> invalidArgs ["User already registered"] Nothing -> invalidArgs ["User already registered"]
_other -> fail "Invalid @regButton@"
mRegAt <- for (courseParticipantRegistration . entityVal <$> mRegistration) $ formatTime SelFormatDateTime 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] cids <- traverse encrypt $ Set.toList selectedUsers :: Handler [CryptoUUIDUser]
redirect (CourseR tid ssh csh CCommR, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cids]) redirect (CourseR tid ssh csh CCommR, [(toPathPiece GetRecipient, toPathPiece cID) | cID <- cids])
(CourseUserDeregister,selectedUsers) -> do (CourseUserDeregister,selectedUsers) -> do
nrDel <- runDB $ deleteWhereCount Sum nrDel <- fmap mconcat . runDB . forM (Set.toList selectedUsers) $ \uid -> do
[ CourseParticipantCourse ==. cid nrDel <- deleteWhereCount
, CourseParticipantUser <-. Set.toList selectedUsers [ CourseParticipantCourse ==. cid
] , CourseParticipantUser ==. uid
]
unless (nrDel == 0) $
audit $ TransactionCourseParticipantDeleted cid uid
return $ Sum nrDel
addMessageI Success $ MsgCourseUsersDeregistered nrDel addMessageI Success $ MsgCourseUsersDeregistered nrDel
redirect $ CourseR tid ssh csh CUsersR redirect $ CourseR tid ssh csh CUsersR
let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|] let headingLong = [whamlet|_{MsgMenuCourseMembers} #{courseName course} #{tid}|]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -60,6 +60,8 @@ import Utils.Sql
import Data.Aeson hiding (Result(..)) import Data.Aeson hiding (Result(..))
import Text.Hamlet (ihamlet) import Text.Hamlet (ihamlet)
import System.FilePath (addExtension)
{- {-
* Implement Handlers * Implement Handlers
@ -439,9 +441,8 @@ getSShowR tid ssh csh shn = do
getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent getSArchiveR :: TermId -> SchoolId -> CourseShorthand -> SheetName -> Handler TypedContent
getSArchiveR tid ssh csh shn = do getSArchiveR tid ssh csh shn = do
MsgRenderer mr <- getMsgRenderer archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack). ap getMessageRender . pure $ MsgSheetArchiveName tid ssh csh shn
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <.> "zip" let sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
sftArchive = CSheetR tid ssh csh shn . SZipR -- used to check access to SheetFileTypes
allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound] allowedSFTs <- filterM (hasReadAccessTo . sftArchive) [minBound..maxBound]
serveZipArchive archiveName $ sheetFilesSFTsQuery tid ssh csh shn allowedSFTs .| C.map entityVal 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 :: TermId -> SchoolId -> CourseShorthand -> SheetName -> SheetFileType -> Handler TypedContent
getSZipR tid ssh csh shn sft = do getSZipR tid ssh csh shn sft = do
MsgRenderer mr <- getMsgRenderer sft' <- ap getMessageRender $ pure sft
let archiveName = (unpack . stripAll $ mr (prependCourseTitle tid ssh csh $ SomeMessage shn)) <> "_" <> (unpack $ toPathPiece sft) <.> "zip" 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 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 Yesod.Colonnade as Yesod
-- import qualified Text.Blaze.Html5.Attributes as HA -- import qualified Text.Blaze.Html5.Attributes as HA
import System.FilePath (addExtension)
-- DEPRECATED: We always show all edits! -- DEPRECATED: We always show all edits!
-- numberOfSubmissionEditDates :: Int64 -- numberOfSubmissionEditDates :: Int64
-- numberOfSubmissionEditDates = 3 -- for debugging only, should be 1 in production. -- 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 :: 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 $ (,) makeSubmissionForm cid msmid uploadMode grouping isLecturer prefillUsers = identifyForm FIDsubmission . renderAForm FormStandard $ (,)
<$> fileUploadForm <$> fileUploadForm (is _Just msmid) (fslI . bool MsgSubmissionFile MsgSubmissionArchive) uploadMode
<*> wFormToAForm submittorsForm <*> wFormToAForm submittorsForm
where 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' :: Markup -> Either UserEmail UserId -> Widget
miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation") miCell' csrf (Left email) = $(widgetFile "widgets/massinput/submissionUsers/cellInvitation")
miCell' csrf (Right uid) = do miCell' csrf (Right uid) = do
@ -574,11 +559,10 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
guardAuthResult =<< evalAccess (CSubmissionR tid ssh csh shn cID CorrectionR) False guardAuthResult =<< evalAccess (CSubmissionR tid ssh csh shn cID CorrectionR) False
let filename sfType' <- ap getMessageRender $ pure sfType
| SubmissionOriginal <- sfType = toPathPiece cID <> "-" <> toPathPiece sfType archiveName <- fmap (flip addExtension (unpack extensionZip) . unpack) . ap getMessageRender . pure $ MsgSubmissionTypeArchiveName tid ssh csh shn cID sfType'
| otherwise = toPathPiece cID
source = do let source = do
submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID submissionID <- lift $ submissionMatchesSheet tid ssh csh shn cID
rating <- lift $ getRating submissionID rating <- lift $ getRating submissionID
@ -593,7 +577,7 @@ getSubArchiveR tid ssh csh shn cID sfType = do
when (sfType == SubmissionCorrected) $ when (sfType == SubmissionCorrected) $
maybe (return ()) (yieldM . ratingFile cID) rating 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 :: TermId -> SchoolId -> CourseShorthand -> SheetName -> CryptoFileNameSubmission -> Handler Html
getSubDelR = postSubDelR getSubDelR = postSubDelR

View File

@ -75,7 +75,7 @@ serveSomeFiles archiveName source = do
[file] -> sendThisFile file [file] -> sendThisFile file
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder
@ -92,7 +92,7 @@ serveZipArchive archiveName source = do
[] -> notFound [] -> notFound
_moreFiles -> do _moreFiles -> do
setContentDisposition' $ Just archiveName setContentDisposition' $ Just archiveName
respondSourceDB "application/zip" $ do respondSourceDB typeZip $ do
let zipComment = T.encodeUtf8 $ pack archiveName let zipComment = T.encodeUtf8 $ pack archiveName
source .| produceZip ZipInfo{..} .| Conduit.map toFlushBuilder 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.Vector as Vector
import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HashMap
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Attoparsec.ByteString.Lazy as A import qualified Data.Attoparsec.ByteString.Lazy as A
@ -43,8 +42,8 @@ instance Exception CsvParseError
typeCsv, typeCsv' :: ContentType typeCsv, typeCsv' :: ContentType
typeCsv = "text/csv" typeCsv = simpleContentType typeCsv'
typeCsv' = BS.intercalate "; " [typeCsv, "charset=UTF-8", "header=present"] typeCsv' = "text/csv; charset=UTF-8; header=present"
extensionCsv :: Extension extensionCsv :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ] 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 Data.Conduit.List as C
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.Utils as E
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
@ -312,6 +313,9 @@ termsAllowedField = selectField $ do
| otherwise = [TermActive ==. True] | otherwise = [TermActive ==. True]
optionsPersistKey termFilter [Desc TermStart] termName optionsPersistKey termFilter [Desc TermStart] termName
termField :: Field Handler TermId
termField = selectField $ optionsPersistKey [] [Asc TermName] termName
termsSetField :: [TermId] -> Field Handler TermId termsSetField :: [TermId] -> Field Handler TermId
termsSetField tids = selectField $ optionsPersistKey [TermName <-. (unTermKey <$> tids)] [Desc TermStart] termName termsSetField tids = selectField $ optionsPersistKey [TermName <-. (unTermKey <$> tids)] [Desc TermStart] termName
-- termsSetField tids = selectFieldList [(unTermKey t, t)| t <- tids ] -- termsSetField tids = selectFieldList [(unTermKey t, t)| t <- tids ]
@ -334,25 +338,35 @@ schoolFieldEnt = selectField $ optionsPersist [] [Asc SchoolName] schoolName
schoolFieldFor :: [SchoolId] -> Field Handler SchoolId schoolFieldFor :: [SchoolId] -> Field Handler SchoolId
schoolFieldFor userSchools = selectField $ optionsPersistKey [SchoolShorthand <-. map unSchoolKey userSchools] [Asc SchoolName] schoolName 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)? studyFeaturesPrimaryFieldFor :: Bool -- ^ Allow user to select `Nothing` (only applies if set of options is nonempty)?
-> [StudyFeaturesId] -> Maybe UserId -> Field Handler (Maybe StudyFeaturesId) -> [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 -- we need a join, so we cannot just use optionsPersistCryptoId
rawOptions <- runDB $ E.select $ E.from $ \(feature `E.InnerJoin` degree `E.InnerJoin` field) -> do 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.^. StudyFeaturesField E.==. field E.^. StudyTermsId
E.on $ feature E.^. StudyFeaturesDegree E.==. degree E.^. StudyDegreeId E.on $ feature E.^. StudyFeaturesDegree E.==. degree E.^. StudyDegreeId
E.where_ $ ((feature E.^. StudyFeaturesId) `E.in_` E.valList oldFeatures) 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) return (feature E.^. StudyFeaturesId, degree, field)
MsgRenderer mr <- getMsgRenderer MsgRenderer mr <- getMsgRenderer
mkOptionList . nonEmptyOptions (mr MsgNoPrimaryStudyField) <$> mapM (procOptions mr) rawOptions mkOptionList . nonEmptyOptions (mr MsgNoStudyField) <$> mapM (procOptions mr) rawOptions
where where
isPrimaryActiveUserStudyFeature feature = case mbuid of isActiveUserStudyFeature feature = case mbuid of
Nothing -> E.val False Nothing -> E.false
(Just uid) -> feature E.^. StudyFeaturesUser E.==. E.val uid Just uid -> feature E.^. StudyFeaturesUser E.==. E.val uid
E.&&. feature E.^. StudyFeaturesValid E.==. E.val True E.&&. feature E.^. StudyFeaturesValid
E.&&. feature E.^. StudyFeaturesType E.==. E.val FieldPrimary 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 :: (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 procOptions mr (E.Value sfid, Entity _dgid sdegree, Entity _stid sterm) = do
@ -372,7 +386,7 @@ studyFeaturesPrimaryFieldFor isOptional oldFeatures mbuid = selectField $ do
nullOption = Option nullOption = Option
{ optionDisplay = emptyOpt { optionDisplay = emptyOpt
, optionInternalValue = Nothing , optionInternalValue = Nothing
, optionExternalValue = "NoPrimaryStudyField" , optionExternalValue = "NoStudyField"
} }
@ -384,7 +398,7 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
[ ( UploadModeNone, pure NoUpload) [ ( UploadModeNone, pure NoUpload)
, ( UploadModeAny , ( UploadModeAny
, UploadAny , 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) <*> aopt extensionRestrictionField (fslI MsgUploadModeExtensionRestriction & setTooltip MsgUploadModeExtensionRestrictionTip) ((prev ^? _Just . _extensionRestriction) <|> fmap Just defaultExtensionRestriction)
) )
, ( UploadModeSpecific , ( UploadModeSpecific
@ -455,6 +469,8 @@ uploadModeForm prev = multiActionA actions (fslI MsgSheetUploadMode) (classifyUp
miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/uploadSpecificFiles/layout") miLayout lLength _ cellWdgts delButtons addWdgts = $(widgetFile "widgets/massinput/uploadSpecificFiles/layout")
submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode submissionModeForm :: Maybe SubmissionMode -> AForm Handler SubmissionMode
submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev submissionModeForm prev = multiActionA actions (fslI MsgSheetSubmissionMode) $ classifySubmissionMode <$> prev
where where
@ -645,12 +661,37 @@ zipFileField doUnpack permittedExtensions = Field{..}
| otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile | otherwise = return . Left $ SomeMessage MsgOnlyUploadOneFile
fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField") fieldView fieldId fieldName attrs _ req = $(widgetFile "widgets/zipFileField")
zipExtensions = mimeExtensions "application/zip" zipExtensions = mimeExtensions typeZip
acceptRestricted = isJust permittedExtensions acceptRestricted = isJust permittedExtensions
accept = Text.intercalate "," . map ("." <>) $ bool [] (Set.toList zipExtensions) doUnpack ++ toListOf (_Just . re _nullable . folded) 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{..} multiFileField permittedFiles' = Field{..}
where where
fieldEnctype = Multipart fieldEnctype = Multipart

View File

@ -25,12 +25,11 @@ gradeSummaryWidget title sts =
hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries hasMarkedPasses = positiveSum $ numMarkedPasses sumSummaries
hasPoints = positiveSum $ numSheetsPoints sumSummaries hasPoints = positiveSum $ numSheetsPoints sumSummaries
hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries hasMarkedPoints = positiveSum $ numMarkedPoints sumSummaries
rowWdgts = [ $(widgetFile "widgets/grading-summary/grading-summary-row") rowWgt (sumHeader, summary) = $(widgetFile "widgets/grading-summary/grading-summary-row") -- diese Funktonsdefinition darf leider nicht im .hamlet stehen!
| (sumHeader,summary) <- rowsShown = [ (MsgSheetTypeNormal' ,normalSummary)
[ (MsgSheetTypeNormal' ,normalSummary) , (MsgSheetTypeBonus' ,bonusSummary)
, (MsgSheetTypeBonus' ,bonusSummary) , (MsgSheetTypeInformational' ,informationalSummary)
, (MsgSheetTypeInformational' ,informationalSummary) ] -- diese Liste könnte auch im .hamlet definiert werden
] ]
in if 0 == numSheets sumSummaries in if 0 == numSheets sumSummaries
then mempty then mempty
else $(widgetFile "widgets/grading-summary/grading-summary") 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)) -> execWriter . forM ratedSubmissions $ \(_rating,_submission,(shn,csh,ssh,tid)) ->
tell (Set.singleton shn, Set.singleton csh, Set.singleton ssh, Set.singleton tid) tell (Set.singleton shn, Set.singleton csh, Set.singleton ssh, Set.singleton tid)
setContentDisposition' $ Just "submissions.zip" archiveName <- ap getMessageRender $ pure MsgSubmissionArchiveName
(<* cleanup) . respondSource "application/zip" . transPipe (runDBRunner dbrunner) $ do setContentDisposition' $ Just ((addExtension `on` unpack) archiveName extensionZip)
(<* cleanup) . respondSource typeZip . transPipe (runDBRunner dbrunner) $ do
let let
fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File fileEntitySource' :: (Rating, Entity Submission, (SheetName,CourseShorthand,SchoolId,TermId)) -> Source (YesodDB UniWorX) File
fileEntitySource' (rating, Entity submissionID Submission{..},(shn,csh,ssh,tid)) = do 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 Control.Monad.Trans.Maybe
, module Colonnade , module Colonnade
, DBSTemplateMode(..)
) where ) where
import Handler.Utils.Table.Pagination.Types import Handler.Utils.Table.Pagination.Types
@ -42,7 +43,7 @@ import Handler.Utils.Form
import Handler.Utils.Csv import Handler.Utils.Csv
import Handler.Utils.ContentDisposition import Handler.Utils.ContentDisposition
import Utils import Utils
import Utils.Lens.TH import Utils.Lens
import Import hiding (pi) import Import hiding (pi)
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
@ -79,7 +80,6 @@ import Text.Hamlet (hamletFile)
import Data.Ratio ((%)) import Data.Ratio ((%))
import Control.Lens hiding ((<.>))
import Control.Lens.Extras (is) import Control.Lens.Extras (is)
import Data.List (elemIndex) import Data.List (elemIndex)
@ -104,6 +104,7 @@ import Data.Semigroup as Sem (Semigroup(..))
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import Handler.Utils.DateTime (formatTimeW)
import qualified Control.Monad.Catch as Catch import qualified Control.Monad.Catch as Catch
@ -428,7 +429,7 @@ data DBEmptyStyle = DBESNoHeading | DBESHeading
instance Default DBEmptyStyle where instance Default DBEmptyStyle where
def = DBESHeading def = DBESHeading
data DBStyle = DBStyle data DBStyle r = DBStyle
{ dbsEmptyStyle :: DBEmptyStyle { dbsEmptyStyle :: DBEmptyStyle
, dbsEmptyMessage :: UniWorXMessage , dbsEmptyMessage :: UniWorXMessage
, dbsAttrs :: [(Text, Text)] , dbsAttrs :: [(Text, Text)]
@ -438,9 +439,13 @@ data DBStyle = DBStyle
-> Widget -> Widget
-> Widget -> Widget
-- ^ Filter UI, Filter Encoding, Filter action, table -- ^ 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 def = DBStyle
{ dbsEmptyStyle = def { dbsEmptyStyle = def
, dbsEmptyMessage = MsgNoTableContent , dbsEmptyMessage = MsgNoTableContent
@ -451,6 +456,7 @@ instance Default DBStyle where
<!-- No Filter UI --> <!-- No Filter UI -->
^{scrolltable} ^{scrolltable}
|] |]
, dbsTemplate = DBSTDefault
} }
defaultDBSFilterLayout :: Widget -- ^ Filter UI 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) , dbtSorting :: Map SortingKey (SortColumn t)
, dbtFilter :: Map FilterKey (FilterColumn t) , dbtFilter :: Map FilterKey (FilterColumn t)
, dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text]) , dbtFilterUI :: Maybe (Map FilterKey [Text]) -> AForm (YesodDB UniWorX) (Map FilterKey [Text])
, dbtStyle :: DBStyle , dbtStyle :: DBStyle r'
, dbtParams :: DBParams m x , dbtParams :: DBParams m x
, dbtCsvEncode :: DBTCsvEncode r' csv , dbtCsvEncode :: DBTCsvEncode r' csv
, dbtCsvDecode :: Maybe (DBTCsvDecode r' k' csv) , dbtCsvDecode :: Maybe (DBTCsvDecode r' k' csv)
@ -1024,29 +1030,40 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
. setParam (wIdent "pagination") Nothing . setParam (wIdent "pagination") Nothing
table' :: HandlerSite m ~ UniWorX => WriterT x m Widget table' :: HandlerSite m ~ UniWorX => WriterT x m Widget
table' = do table' = let
let columnCount :: Int64
columnCount = olength64 $ getColonnade dbtColonnade
genHeaders SortableP{..} = forM (toSortable . oneColonnadeHead <$> getColonnade dbtColonnade) $ \Sortable{..} -> do genHeaders SortableP{..} = forM (toSortable . oneColonnadeHead <$> getColonnade dbtColonnade) $ \Sortable{..} -> do
widget <- sortableContent ^. cellContents widget <- sortableContent ^. cellContents
let let
directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ] directions = [dir | SortingSetting k dir <- psSorting, Just k == sortableKey ]
isSortable = isJust sortableKey isSortable = isJust sortableKey
isSorted = (`elem` directions) isSorted dir = fromMaybe False $ (==) <$> (SortingSetting <$> sortableKey <*> pure dir) <*> listToMaybe psSorting
attrs = sortableContent ^. cellAttrs attrs = sortableContent ^. cellAttrs
piSorting' = [ sSet | sSet <- fromMaybe [] piSorting, Just (sortKey sSet) /= sortableKey ] piSorting' = [ sSet | sSet <- fromMaybe [] piSorting, Just (sortKey sSet) /= sortableKey ]
return $(widgetFile "table/cell/header") case dbsTemplate of
DBSTCourse _ _ _ _ -> return $(widgetFile "table/course/header")
columnCount :: Int64 DBSTDefault -> return $(widgetFile "table/cell/header")
columnCount = olength64 $ getColonnade dbtColonnade in do
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable
wHeaders <- maybe (return Nothing) (fmap Just . genHeaders) pSortable case dbsTemplate of
DBSTCourse c l r s -> do
wRows <- forM rows $ \row' -> forM (oneColonnadeEncode <$> getColonnade dbtColonnade) $ \(($ row') -> cell') -> do wRows <- forM rows $ \row' -> let
widget <- cell' ^. cellContents Course{..} = row' ^. c . _entityVal
let attrs = cell' ^. cellAttrs lecturerUsers = row' ^. l
return $(widgetFile "table/cell/body") courseLecturers = userSurname . entityVal <$> lecturerUsers
isRegistered = row' ^. r
return $(widgetFile "table/colonnade") 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 pageCount
| PagesizeLimit l <- psLimit | PagesizeLimit l <- psLimit

View File

@ -2,7 +2,8 @@
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Handler.Utils.Zip module Handler.Utils.Zip
( ZipError(..) ( typeZip, extensionZip
, ZipError(..)
, ZipInfo(..) , ZipInfo(..)
, produceZip , produceZip
, consumeZip , consumeZip
@ -27,6 +28,16 @@ import Data.Time.LocalTime (localTimeToUTC, utcToLocalTime)
import Data.List (dropWhileEnd) 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 instance Default ZipInfo where
def = ZipInfo 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 -- 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 :: (MonadLogger m, MonadResource m, MonadThrow m, MonadIO m) => FileInfo -> Source m File
sourceFiles fInfo sourceFiles fInfo
| mimeType == "application/zip" = do | ((==) `on` simpleContentType) mimeType typeZip = do
$logInfoS "sourceFiles" "Unpacking ZIP" $logInfoS "sourceFiles" "Unpacking ZIP"
fileSource fInfo =$= void consumeZip fileSource fInfo =$= void consumeZip
| otherwise = do | otherwise = do

View File

@ -34,8 +34,11 @@ import Text.Shakespeare.Text (st)
import Control.Monad.Trans.Reader (mapReaderT) import Control.Monad.Trans.Reader (mapReaderT)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
import Utils (exceptT, allM, whenIsJust, guardM) import Utils (exceptT, allM, whenIsJust, guardM)
import Utils.Lens (_NoUpload)
import Utils.DB (getKeyBy) import Utils.DB (getKeyBy)
import Control.Lens
import Numeric.Natural import Numeric.Natural
import qualified Net.IP as IP import qualified Net.IP as IP
@ -398,6 +401,50 @@ customMigrations = Map.fromListWith (>>)
updateTransactionInfo _ = return () updateTransactionInfo _ = return ()
runConduit $ getLogEntries .| C.mapM_ updateTransactionInfo 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 | AuthCourseRegistered
| AuthTutorialRegistered | AuthTutorialRegistered
| AuthExamRegistered | AuthExamRegistered
| AuthExamResult
| AuthParticipant | AuthParticipant
| AuthTime | AuthTime
| AuthAllocationTime | AuthAllocationTime

View File

@ -54,6 +54,9 @@ data Icon
| IconSFTSolution -- for SheetFileType only | IconSFTSolution -- for SheetFileType only
| IconSFTMarking -- for SheetFileType only | IconSFTMarking -- for SheetFileType only
| IconEmail | IconEmail
| IconRegisterTemplate
| IconApplyTrue
| IconApplyFalse
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
iconText :: Icon -> Text iconText :: Icon -> Text
@ -82,6 +85,9 @@ iconText = \case
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope" IconEmail -> "envelope"
IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash"
instance Universe Icon instance Universe Icon
instance Finite Icon instance Finite Icon
@ -150,6 +156,10 @@ iconEnrol :: Bool -> Markup
iconEnrol True = icon IconEnrolTrue iconEnrol True = icon IconEnrolTrue
iconEnrol False = icon IconEnrolFalse iconEnrol False = icon IconEnrolFalse
iconApply :: Bool -> Markup
iconApply True = icon IconApplyTrue
iconApply False = icon IconApplyFalse
iconExamRegister :: Bool -> Markup iconExamRegister :: Bool -> Markup
iconExamRegister True = icon IconExamRegisterTrue iconExamRegister True = icon IconExamRegisterTrue
iconExamRegister False = icon IconExamRegisterFalse 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 :: MonoFoldable mono => Prism' mono (NonNull mono)
_nullable = prism' toNullable fromNullable _nullable = prism' toNullable fromNullable
_SchoolId :: Iso' SchoolId SchoolShorthand
_SchoolId = iso unSchoolKey SchoolKey
----------------------------------- -----------------------------------
-- Lens Definitions for our Types -- Lens Definitions for our Types
@ -120,6 +123,7 @@ makePrisms ''HandlerContents
makePrisms ''ErrorResponse makePrisms ''ErrorResponse
makePrisms ''UploadMode
makeLenses_ ''UploadMode makeLenses_ ''UploadMode
makeLenses_ ''SubmissionMode makeLenses_ ''SubmissionMode

View File

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

View File

@ -79,13 +79,38 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<div> <div>
\ <em>Achtung:</em> \ <em>Achtung:</em>
\ Abmeldung nur bis #{dereg} erlaubt. \ 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 $if registrationOpen || isJust mRegAt
<dt .deflist__dt> <dt .deflist__dt>
_{MsgCourseRegistration}
<dd .deflist__dd> <dd .deflist__dd>
<div .course__registration> <div .course__registration>
$if registrationOpen $if registrationOpen
$# regForm is defined through templates/widgets/registerForm $# regForm is defined through templates/widgets/registerForm
^{regForm} ^{regForm}
$if isJust mApplication && courseApplicationsRequired course
<p>
_{MsgCourseApplicationDeleteToEdit}
$else
$if isJust mRegAt
<p>
_{MsgCourseRegistrationDeleteToEdit}
$maybe date <- mRegAt $maybe date <- mRegAt
_{MsgRegisteredSince} #{date} _{MsgRegisteredSince} #{date}
<dt .deflist__dt> <dt .deflist__dt>
@ -106,16 +131,3 @@ $# $if NTop (Just 0) < NTop (courseCapacity course)
<dd .deflist__dd> <dd .deflist__dd>
^{tutorialTable} ^{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 */
.scrolltable { .scrolltable {
overflow: auto; overflow: auto;
}
.scrolltable--bordered {
box-shadow: 0 0 1px 1px var(--color-grey-light); box-shadow: 0 0 1px 1px var(--color-grey-light);
} }

View File

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

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 <h2>Bekannte Probleme in Bearbeitung
<dl .deflist> <dl .deflist>
<dt .deflist__dt>Klausuren #{iconNew} <dt .deflist__dt>#{iconNew} Zentralanmeldung
<dd .deflist__dd> <dd .deflist__dd>
Klausuren werden ab sofort teilweise unterstüzt. <p>
Der genaue Stand der Entwicklung ist weiter unter auf dieser Veranstalter können eigene Veranstaltungen zu verschiedenen Zentralanmeldungen hinzufügen.
Seite in einem eigenem Abschnitt detailliert. 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 <dt .deflist__dt>Benachrichtigungen
<dd .deflist__dd> <dd .deflist__dd>
@ -41,10 +61,20 @@ $newline text
Dabei werden vor allem Kurskürzel und die Kursbeschreibung übernommen; Dabei werden vor allem Kurskürzel und die Kursbeschreibung übernommen;
nicht jedoch Übungsblätter, Klausuren oder Anmeldungen. nicht jedoch Übungsblätter, Klausuren oder Anmeldungen.
<pr> <p>
Die Kursbeschreibung kann in Html verfasst werden und Die Kursbeschreibung kann in Html verfasst werden und
<em>sollte die Modulbeschreibung enthalten! <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 <dt .deflist__dt> Materialzugriff
<dd .deflist__dd> <dd .deflist__dd>
Der Zugriff auf Übungsblätter, Folien und andere Materialien 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. Um die Anmeldung in beliebig viele Tutoriumsgruppen zuzulassen können alle Registrierungs-Gruppen leer gelassen werden.
<section> <section id="exams">
<h2> Klausuren <h2> Klausuren und Prüfungen
<p> Das Verwalten von Klausuren und Notenmeldungen wurde nun teilweise implementiert und ist ab sofort einsetzbar. <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> <dl .deflist>
<dt .deflist__dt> Anlegen/Editieren <dt .deflist__dt> Anlegen/Editieren
<dd .deflist__dd> <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. 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. 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 <dt .deflist__dt> #{iconProblem} Prüfungszuteilung
<dd .deflist__dd> <dd .deflist__dd>
Auf Wunsch kann Uni2work die Zuteilung der Teilnehmer auf die Prüfungen (Räume bzw. Prüfungstermine) 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. 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 <dt .deflist__dt> #{iconProblem} Korrekturen
<dd .deflist__dd> <dd .deflist__dd>
<p> <p>
Korrekturen können derzeit noch nicht eingetragen werden. Korrekturen können derzeit noch nicht eingetragen werden.
Die Realisierung sollte in wenigen Wochen erfolgen. Die Realisierung sollte bald erfolgen.
<p> <p>
Die Eintragung von Korrekturen erfolgt immer pro Teilaufgabe. Die Eintragung von Korrekturen erfolgt immer pro Teilaufgabe.
Optional kann aus der erreichten Punktesumme dann automatisch eine Gesamtnote berechnet werden. 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, Es werden verschiedene Möglichkeiten angebotenen werden,
die erzielten Bewertungen der Hausübungen die erzielten Bewertungen der Hausübungen
unter einstellbaren Bedingungen 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 <dt .deflist__dt> #{iconProblem} Notenmeldung
<dd .deflist__dd> <dd .deflist__dd>
@ -319,6 +354,35 @@ $newline text
Prüfungsamt übermitteln. Für größere Änderungen kann das Prüfungsamt Prüfungsamt übermitteln. Für größere Änderungen kann das Prüfungsamt
die Klausur auch wieder an den Dozenten zurück übergeben; 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. 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> <section>
<h2>Sonstiges <h2>Sonstiges
<dl .deflist> <dl .deflist>

View File

@ -1,8 +1,8 @@
$newline never $newline never
<p> <p>
Stand: July 2019 Stand: August 2019
<ul> <ul>
<li> <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> <li>
Format von Bewertungsdateien ist noch provisorisch Format von Bewertungsdateien ist noch provisorisch

View File

@ -134,7 +134,7 @@
Benutzerdaten bleiben prinzipiell so lange gespeichert, Benutzerdaten bleiben prinzipiell so lange gespeichert,
bis ein Institutsadministrator über die Exmatrikulation informiert wurde. bis ein Institutsadministrator über die Exmatrikulation informiert wurde.
Dann wird der Account mit einer angemessenen zeitverzögerung gelöscht. 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> <li>
Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht. Bei gemeinsamen Gruppenabgaben wird nur die Zuordnung zu diesem Benutzer gelöscht.

View File

@ -1,5 +1,5 @@
$newline never $newline never
<div .scrolltable> <div .scrolltable .scrolltable--bordered>
<table *{dbsAttrs'}> <table *{dbsAttrs'}>
$maybe wHeaders' <- wHeaders $maybe wHeaders' <- wHeaders
<thead> <thead>
@ -17,5 +17,4 @@ $newline never
$forall row <- wRows $forall row <- wRows
<tr .table__row> <tr .table__row>
$forall widget <- row $forall widget <- row
$# cell/body.hamlet
^{widget} ^{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} _{MsgKnownBugs}
^{knownBugs} ^{knownBugs}
<section>
<h2>
_{MsgImplementationDetails}
^{implementation}
<section> <section>
<h2> <h2>
_{MsgVersionHistory} _{MsgVersionHistory}

View File

@ -6,7 +6,7 @@
werden Benutzer hiermit vollständig aus der Live-Datenbank mit werden Benutzer hiermit vollständig aus der Live-Datenbank mit
<code>DELETE CASCADE uid <code>DELETE CASCADE uid
gelöscht. 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! aufbewahrt werden!
<p> <p>
Benutzer können sich mit Ihrem Campus-Account Benutzer können sich mit Ihrem Campus-Account

View File

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

View File

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

View File

@ -16,7 +16,13 @@ instance Arbitrary (Route Auth) where
] ]
instance Arbitrary (Route EmbeddedStatic) 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 instance Arbitrary CourseR where
arbitrary = genericArbitrary arbitrary = genericArbitrary
@ -42,6 +48,10 @@ instance Arbitrary ExamR where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink
instance Arbitrary CourseApplicationR where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary (Route UniWorX) where instance Arbitrary (Route UniWorX) where
arbitrary = genericArbitrary arbitrary = genericArbitrary
shrink = genericShrink shrink = genericShrink