Skip to content
Snippets Groups Projects
Commit 1b72b309 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '258-add-export-functionality-to-course-book' into 'master'

Resolve "Add export functionality to course book"

Closes #258

See merge request !422
parents 28ec5cbc fbca4c69
No related branches found
No related tags found
1 merge request!422Resolve "Add export functionality to course book"
Pipeline #194367 failed
Showing
with 691 additions and 528 deletions
...@@ -16,6 +16,11 @@ If you're upgrading from 3.x, there is now a migration path to use. ...@@ -16,6 +16,11 @@ If you're upgrading from 3.x, there is now a migration path to use.
Therefore, please install ``AlekSIS-App-Lesrooster`` which now Therefore, please install ``AlekSIS-App-Lesrooster`` which now
includes parts of the legacy Chronos and the migration path. includes parts of the legacy Chronos and the migration path.
Added
~~~~~
* Configurable PDF export of the coursebook for one or more groups.
`4.0.0.dev8`_ - 2024-11-15 `4.0.0.dev8`_ - 2024-11-15
-------------------------- --------------------------
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
use-deep-search use-deep-search
> >
<template #additionalActions="{ attrs, on }"> <template #additionalActions="{ attrs, on }">
<coursebook-filters :page-type="pageType" v-model="filters" /> <coursebook-controls :page-type="pageType" v-model="filters" />
<v-expand-transition> <v-expand-transition>
<v-card <v-card
outlined outlined
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
:subjects="subjects" :subjects="subjects"
:documentation="item" :documentation="item"
:affected-query="lastQuery" :affected-query="lastQuery"
:value="(selectedParticipations[item.id] ??= [])" :value="selectedParticipations[item.id] ??= []"
@input="selectParticipation(item.id, $event)" @input="selectParticipation(item.id, $event)"
/> />
</template> </template>
...@@ -69,9 +69,7 @@ ...@@ -69,9 +69,7 @@
<DocumentationLoader /> <DocumentationLoader />
</template> </template>
</infinite-scrolling-date-sorted-c-r-u-d-iterator> </infinite-scrolling-date-sorted-c-r-u-d-iterator>
<absence-creation-dialog <absence-creation-dialog :absence-reasons="absenceReasons" />
:absence-reasons="absenceReasons"
/>
</div> </div>
</template> </template>
...@@ -79,7 +77,7 @@ ...@@ -79,7 +77,7 @@
import InfiniteScrollingDateSortedCRUDIterator from "aleksis.core/components/generic/InfiniteScrollingDateSortedCRUDIterator.vue"; import InfiniteScrollingDateSortedCRUDIterator from "aleksis.core/components/generic/InfiniteScrollingDateSortedCRUDIterator.vue";
import { documentationsForCoursebook } from "./coursebook.graphql"; import { documentationsForCoursebook } from "./coursebook.graphql";
import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue"; import AbsenceReasonButtons from "aleksis.apps.kolego/components/AbsenceReasonButtons.vue";
import CoursebookFilters from "./CoursebookFilters.vue"; import CoursebookControls from "./CoursebookControls.vue";
import CoursebookLoader from "./CoursebookLoader.vue"; import CoursebookLoader from "./CoursebookLoader.vue";
import DocumentationModal from "./documentation/DocumentationModal.vue"; import DocumentationModal from "./documentation/DocumentationModal.vue";
import DocumentationAbsencesModal from "./absences/DocumentationAbsencesModal.vue"; import DocumentationAbsencesModal from "./absences/DocumentationAbsencesModal.vue";
...@@ -95,7 +93,7 @@ export default { ...@@ -95,7 +93,7 @@ export default {
components: { components: {
DocumentationLoader, DocumentationLoader,
AbsenceReasonButtons, AbsenceReasonButtons,
CoursebookFilters, CoursebookControls,
CoursebookLoader, CoursebookLoader,
DocumentationModal, DocumentationModal,
DocumentationAbsencesModal, DocumentationAbsencesModal,
......
<script setup>
import CoursebookPrintDialog from "./CoursebookPrintDialog.vue";
</script>
<template> <template>
<div <div
class="d-flex flex-column flex-sm-row flex-nowrap flex-grow-1 justify-end gap align-stretch" class="d-flex flex-column flex-sm-row flex-nowrap flex-grow-1 justify-end gap align-stretch"
...@@ -56,14 +60,22 @@ ...@@ -56,14 +60,22 @@
hide-details hide-details
/> />
</div> </div>
<v-btn <div class="d-flex flex-column gap">
outlined <v-btn
color="primary" outlined
:loading="selectLoading" color="primary"
@click="togglePageType()" :loading="selectLoading"
> @click="togglePageType()"
{{ pageTypeButtonText }} >
</v-btn> {{ pageTypeButtonText }}
</v-btn>
<coursebook-print-dialog
v-if="pageType === 'documentations'"
:loading="selectLoading"
:available-groups="groups"
:value="currentGroups"
/>
</div>
</div> </div>
</template> </template>
...@@ -125,6 +137,13 @@ export default { ...@@ -125,6 +137,13 @@ export default {
o.id === this.value.objId, o.id === this.value.objId,
); );
}, },
currentGroups() {
return this.groups.filter(
(o) =>
TYPENAMES_TO_TYPES[o.__typename] === this.value.objType &&
o.id === this.value.objId,
);
},
pageTypeButtonText() { pageTypeButtonText() {
if (this.value.pageType === "documentations") { if (this.value.pageType === "documentations") {
return this.$t("alsijil.coursebook.filter.page_type.absences"); return this.$t("alsijil.coursebook.filter.page_type.absences");
......
<script setup>
import MobileFullscreenDialog from "aleksis.core/components/generic/dialogs/MobileFullscreenDialog.vue";
import SecondaryActionButton from "aleksis.core/components/generic/buttons/SecondaryActionButton.vue";
import PrimaryActionButton from "aleksis.core/components/generic/buttons/PrimaryActionButton.vue";
import CancelButton from "aleksis.core/components/generic/buttons/CancelButton.vue";
</script>
<template>
<mobile-fullscreen-dialog v-model="dialog">
<template #activator>
<secondary-action-button
i18n-key="alsijil.coursebook.print.button"
icon-text="$print"
:loading="loading"
@click="dialog = true"
:disabled="dialog"
/>
</template>
<template #title>
{{ $t("alsijil.coursebook.print.title") }}
</template>
<template #content>
{{ $t("alsijil.coursebook.print.groups") }}
<v-autocomplete
:items="availableGroups"
item-text="name"
item-value="id"
:value="value"
@input="setGroupSelection"
@click:clear="setGroupSelection"
multiple
chips
deletable-chips
/>
<div class="d-flex flex-column">
{{ $t("alsijil.coursebook.print.include") }}
<v-checkbox
v-model="includeCover"
:label="$t('alsijil.coursebook.print.include_cover')"
/>
<v-checkbox
v-model="includeAbbreviations"
:label="$t('alsijil.coursebook.print.include_abbreviations')"
/>
<v-checkbox
v-model="includeMembersTable"
:label="$t('alsijil.coursebook.print.include_members_table')"
/>
<v-checkbox
v-model="includeTeachersAndSubjectsTable"
:label="
$t('alsijil.coursebook.print.include_teachers_and_subjects_table')
"
/>
<v-checkbox
v-model="includePersonOverviews"
:label="$t('alsijil.coursebook.print.include_person_overviews')"
/>
<v-checkbox
v-model="includeCoursebook"
:label="$t('alsijil.coursebook.print.include_coursebook')"
/>
</div>
</template>
<template #actions>
<!-- TODO: Should cancel reset state? -->
<cancel-button @click="dialog = false" />
<primary-action-button
i18n-key="alsijil.coursebook.print.button"
icon-text="$print"
:disabled="!valid"
@click="print"
/>
</template>
</mobile-fullscreen-dialog>
</template>
<script>
/**
* This component provides a dialog for configuring the coursebook-printout
*/
export default {
name: "CoursebookPrintDialog",
props: {
/**
* Groups available for selection
*/
availableGroups: {
type: Array,
required: true,
},
/**
* Initially selected groups
*/
value: {
type: Array,
required: false,
default: () => [],
},
/**
* Loading state
*/
loading: {
type: Boolean,
required: false,
default: false,
},
},
emits: ["input"],
data() {
return {
dialog: false,
currentGroupSelection: [],
includeCover: true,
includeAbbreviations: true,
includeMembersTable: true,
includeTeachersAndSubjectsTable: true,
includePersonOverviews: true,
includeCoursebook: true,
};
},
computed: {
selectedGroups() {
if (this.currentGroupSelection.length == 0) {
return this.value.map((group) => group.id);
} else {
return this.currentGroupSelection;
}
},
valid() {
return (
this.selectedGroups.length > 0 &&
(this.includeMembersTable ||
this.includeTeachersAndSubjectsTable ||
this.includePersonOverviews ||
this.includeCoursebook)
);
},
},
methods: {
setGroupSelection(groups) {
this.$emit("input", groups);
this.currentGroupSelection = groups;
},
print() {
this.$router.push({
name: "alsijil.coursebook_print",
params: {
groupIds: this.selectedGroups,
},
query: {
cover: this.includeCover,
abbreviations: this.includeAbbreviations,
members_table: this.includeMembersTable,
teachers_and_subjects_table: this.includeTeachersAndSubjectsTable,
person_overviews: this.includePersonOverviews,
coursebook: this.includeCoursebook,
},
});
},
},
};
</script>
...@@ -94,6 +94,14 @@ export default { ...@@ -94,6 +94,14 @@ export default {
}, },
], ],
}, },
{
path: "print/groups/:groupIds+/",
component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
name: "alsijil.coursebook_print",
props: {
byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
},
},
{ {
path: "extra_marks/", path: "extra_marks/",
component: () => import("./components/extra_marks/ExtraMarks.vue"), component: () => import("./components/extra_marks/ExtraMarks.vue"),
......
...@@ -98,8 +98,19 @@ ...@@ -98,8 +98,19 @@
"person_page": { "person_page": {
"title": "Kursbuch · Statistiken · {fullName}", "title": "Kursbuch · Statistiken · {fullName}",
"summary": "Zusammenfassung" "summary": "Zusammenfassung"
}, }
"title_plural": "Statistiken" },
"print": {
"button": "Drucken",
"title": "Kursbuchausdruck",
"groups": "Gruppen",
"include": "Abschnitte",
"include_cover": "Deckblatt",
"include_abbreviations": "Abkürzungen",
"include_members_table": "Tabelle aller Gruppenmitglieder mit Statistiken",
"include_teachers_and_subjects_table": "Tabelle mit Lehrkräften und Fächern",
"include_person_overviews": "Detailseiten für alle Gruppenmitglieder",
"include_coursebook": "Kursbuch"
} }
}, },
"excuse_types": { "excuse_types": {
......
...@@ -129,6 +129,18 @@ ...@@ -129,6 +129,18 @@
"title": "Error: no person | Successfully marked {name} as {reason} | Successfully marked {n} people as {reason}", "title": "Error: no person | Successfully marked {name} as {reason} | Successfully marked {n} people as {reason}",
"description": "Do you want to mark them as {reason} for the rest of their day?", "description": "Do you want to mark them as {reason} for the rest of their day?",
"action_button": "Extend absence" "action_button": "Extend absence"
},
"print": {
"button": "Print",
"title": "Print Coursebook",
"groups": "Groups",
"include": "Parts to include",
"include_cover": "Cover",
"include_abbreviations": "Abbreviations",
"include_members_table": "Members Table",
"include_teachers_and_subjects_table": "Teachers and Subjects Table",
"include_person_overviews": "Person Overviews",
"include_coursebook": "Coursebook"
} }
}, },
"personal_notes": { "personal_notes": {
......
...@@ -115,6 +115,19 @@ def annotate_person_statistics( ...@@ -115,6 +115,19 @@ def annotate_person_statistics(
return persons return persons
def annotate_person_statistics_from_documentations(
persons: QuerySet[Person], docs: QuerySet[Documentation]
) -> QuerySet[Person]:
"""Annotate a queryset of persons with class register statistics from documentations."""
docs = list(docs.values_list("pk", flat=True))
return annotate_person_statistics(
persons,
Q(participations__related_documentation__in=docs),
Q(new_personal_notes__documentation__in=docs),
ignore_filters=len(docs) == 0,
)
def annotate_person_statistics_for_school_term( def annotate_person_statistics_for_school_term(
persons: QuerySet[Person], school_term: SchoolTerm, group: Group | None = None persons: QuerySet[Person], school_term: SchoolTerm, group: Group | None = None
) -> QuerySet[Person]: ) -> QuerySet[Person]:
...@@ -133,10 +146,4 @@ def annotate_person_statistics_for_school_term( ...@@ -133,10 +146,4 @@ def annotate_person_statistics_for_school_term(
) )
) )
) )
docs = list(documentations.values_list("pk", flat=True)) return annotate_person_statistics_from_documentations(persons, documentations)
return annotate_person_statistics(
persons,
Q(participations__related_documentation__in=docs),
Q(new_personal_notes__documentation__in=docs),
ignore_filters=len(docs) == 0,
)
from datetime import datetime from datetime import datetime
from typing import Optional from typing import List, Optional
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
...@@ -117,6 +117,9 @@ class Documentation(CalendarEvent): ...@@ -117,6 +117,9 @@ class Documentation(CalendarEvent):
if self.course: if self.course:
return self.course.groups.all() return self.course.groups.all()
def get_teachers_short_names(self) -> List[str]:
return [teacher.short_name or teacher.name for teacher in self.teachers.all()]
def __str__(self) -> str: def __str__(self) -> str:
start_datetime = CalendarEvent.value_start_datetime(self) start_datetime = CalendarEvent.value_start_datetime(self)
end_datetime = CalendarEvent.value_end_datetime(self) end_datetime = CalendarEvent.value_end_datetime(self)
......
...@@ -54,10 +54,6 @@ td.lesson-notes span.lesson-note-late { ...@@ -54,10 +54,6 @@ td.lesson-notes span.lesson-note-late {
color: #ff9933; color: #ff9933;
} }
td.lesson-notes span.lesson-note-excused {
color: #009933;
}
table.person-info { table.person-info {
border: none; border: none;
} }
......
# from copy import deepcopy from datetime import date
# from datetime import date, timedelta from typing import List, Optional
# from django.db.models import Q from django.db.models import Prefetch, Q
# from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
# from calendarweek import CalendarWeek from celery.result import allow_join_result
# from celery.result import allow_join_result from celery.states import SUCCESS
# from celery.states import SUCCESS
# from aleksis.core.models import Group, PDFFile from aleksis.apps.cursus.models import Course
from aleksis.apps.kolego.models.absence import AbsenceReason
from aleksis.core.models import Group, PDFFile
from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task from aleksis.core.util.celery_progress import ProgressRecorder, recorded_task
from aleksis.core.util.pdf import generate_pdf_from_template
# from aleksis.core.util.pdf import generate_pdf_from_template from .model_extensions import annotate_person_statistics_from_documentations
from .models import Documentation, ExtraMark, NewPersonalNote, ParticipationStatus
# from .models import ExtraMark
@recorded_task @recorded_task
def generate_full_register_printout(group: int, file_object: int, recorder: ProgressRecorder): def generate_full_register_printout(
"""Generate a full register printout as PDF for a group.""" groups: List[int],
file_object: int,
recorder: ProgressRecorder,
# context = {} include_cover: Optional[bool] = True,
include_abbreviations: Optional[bool] = True,
# _number_of_steps = 8 include_members_table: Optional[bool] = True,
include_teachers_and_subjects_table: Optional[bool] = True,
# recorder.set_progress(1, _number_of_steps, _("Load data ...")) include_person_overviews: Optional[bool] = True,
include_coursebook: Optional[bool] = True,
# group = Group.objects.get(pk=group) ):
# file_object = PDFFile.objects.get(pk=file_object) """Generate a configurable register printout as PDF for a group."""
# groups_q = ( def prefetch_notable_participations(select_related=None, prefetch_related=None):
# Q(lesson_period__lesson__groups=group) if not select_related:
# | Q(lesson_period__lesson__groups__parent_groups=group) select_related = []
# | Q(extra_lesson__groups=group) if not prefetch_related:
# | Q(extra_lesson__groups__parent_groups=group) prefetch_related = []
# | Q(event__groups=group) return Prefetch(
# | Q(event__groups__parent_groups=group) "participations",
# ) to_attr="notable_participations",
# personal_notes = ( queryset=ParticipationStatus.objects.filter(
# PersonalNote.objects.prefetch_related( Q(absence_reason__tags__short_name="class_register") | Q(tardiness__isnull=False)
# "lesson_period__substitutions", "lesson_period__lesson__teachers" )
# ) .select_related("absence_reason", *select_related)
# .not_empty() .prefetch_related(*prefetch_related),
# .filter(groups_q) )
# .filter(groups_of_person=group)
# ) def prefetch_personal_notes(name, select_related=None, prefetch_related=None):
# documentations = LessonDocumentation.objects.not_empty().filter(groups_q) if not select_related:
select_related = []
# recorder.set_progress(2, _number_of_steps, _("Sort data ...")) if not prefetch_related:
prefetch_related = []
# sorted_documentations = {"extra_lesson": {}, "event": {}, "lesson_period": {}} return Prefetch(
# sorted_personal_notes = {"extra_lesson": {}, "event": {}, "lesson_period": {}, "person": {}} name,
# for documentation in documentations: queryset=NewPersonalNote.objects.filter(Q(note__gt="") | Q(extra_mark__isnull=False))
# key = documentation.register_object.label_ .select_related("extra_mark", *select_related)
# sorted_documentations[key][documentation.register_object_key] = documentation .prefetch_related(*prefetch_related),
)
# for note in personal_notes:
# key = note.register_object.label_ context = {}
# sorted_personal_notes[key].setdefault(note.register_object_key, [])
# sorted_personal_notes[key][note.register_object_key].append(note) context["include_cover"] = include_cover
# sorted_personal_notes["person"].setdefault(note.person.pk, []) context["include_abbreviations"] = include_abbreviations
# sorted_personal_notes["person"][note.person.pk].append(note) context["include_members_table"] = include_members_table
context["include_teachers_and_subjects_table"] = include_teachers_and_subjects_table
# recorder.set_progress(3, _number_of_steps, _("Load lesson data ...")) context["include_person_overviews"] = include_person_overviews
context["include_coursebook"] = include_coursebook
# # Get all lesson periods for the selected group
# lesson_periods = LessonPeriod.objects.filter_group(group).distinct() context["today"] = date.today()
# events = Event.objects.filter_group(group).distinct()
# extra_lessons = ExtraLesson.objects.filter_group(group).distinct() _number_of_steps = 5 + len(groups)
# weeks = CalendarWeek.weeks_within(group.school_term.date_start, group.school_term.date_end)
recorder.set_progress(1, _number_of_steps, _("Loading data ..."))
# register_objects_by_day = {}
# for extra_lesson in extra_lessons: groups = Group.objects.filter(pk__in=groups).order_by("name")
# day = extra_lesson.date
# register_objects_by_day.setdefault(day, []).append( if include_cover:
# ( groups = groups.select_related("school_term")
# extra_lesson,
# sorted_documentations["extra_lesson"].get(extra_lesson.pk), if include_abbreviations or include_members_table:
# sorted_personal_notes["extra_lesson"].get(extra_lesson.pk, []), context["absence_reasons"] = AbsenceReason.objects.filter(
# None, tags__short_name="class_register", count_as_absent=True
# ) )
# ) context["absence_reasons_not_counted"] = AbsenceReason.objects.filter(
tags__short_name="class_register", count_as_absent=False
# for event in events: )
# day_number = (event.date_end - event.date_start).days + 1 context["extra_marks"] = ExtraMark.objects.all()
# for i in range(day_number):
# day = event.date_start + timedelta(days=i) if include_members_table or include_person_overviews:
# event_copy = deepcopy(event) groups = groups.prefetch_related("members")
# event_copy.annotate_day(day)
if include_teachers_and_subjects_table:
# # Skip event days if it isn't inside the timetable schema groups = groups.prefetch_related(
# if not (event_copy.raw_period_from_on_day and event_copy.raw_period_to_on_day): Prefetch("courses", queryset=Course.objects.select_related("subject")),
# continue "courses__teachers",
"child_groups",
# register_objects_by_day.setdefault(day, []).append( Prefetch("child_groups__courses", queryset=Course.objects.select_related("subject")),
# ( "child_groups__courses__teachers",
# event_copy, )
# sorted_documentations["event"].get(event.pk),
# sorted_personal_notes["event"].get(event.pk, []), recorder.set_progress(2, _number_of_steps, _("Loading groups ..."))
# None,
# ) for i, group in enumerate(groups, start=1):
# ) recorder.set_progress(
2 + i, _number_of_steps, _(f"Loading group {group.short_name or group.name} ...")
# recorder.set_progress(4, _number_of_steps, _("Sort lesson data ...")) )
# weeks = CalendarWeek.weeks_within( if include_members_table or include_person_overviews or include_coursebook:
# group.school_term.date_start, documentations = Documentation.objects.filter(
# group.school_term.date_end, Q(datetime_start__date__gte=group.school_term.date_start)
# ) & Q(datetime_end__date__lte=group.school_term.date_end)
& Q(
# for lesson_period in lesson_periods: pk__in=Documentation.objects.filter(course__groups=group)
# for week in weeks: .values_list("pk", flat=True)
# day = week[lesson_period.period.weekday] .union(
Documentation.objects.filter(
# if ( course__groups__parent_groups=group
# lesson_period.lesson.validity.date_start ).values_list("pk", flat=True)
# <= day )
# <= lesson_period.lesson.validity.date_end )
# ): )
# filtered_documentation = sorted_documentations["lesson_period"].get(
# f"{lesson_period.pk}_{week.week}_{week.year}" if include_members_table or include_person_overviews:
# ) group.members_with_stats = annotate_person_statistics_from_documentations(
# filtered_personal_notes = sorted_personal_notes["lesson_period"].get( group.members.all(), documentations
# f"{lesson_period.pk}_{week.week}_{week.year}", [] )
# )
if include_person_overviews:
# substitution = lesson_period.get_substitution(week) doc_query_set = documentations.select_related("subject").prefetch_related("teachers")
group.members_with_stats = group.members_with_stats.prefetch_related(
# register_objects_by_day.setdefault(day, []).append( prefetch_notable_participations(
# (lesson_period, filtered_documentation, filtered_personal_notes, substitution) prefetch_related=[Prefetch("related_documentation", queryset=doc_query_set)]
# ) ),
prefetch_personal_notes(
# recorder.set_progress(5, _number_of_steps, _("Load statistics ...")) "new_personal_notes",
prefetch_related=[Prefetch("documentation", queryset=doc_query_set)],
# persons = group.members.prefetch_related(None).select_related(None) ),
# persons = group.generate_person_list_with_class_register_statistics(persons) )
# prefetched_persons = [] if include_teachers_and_subjects_table:
# for person in persons: group.as_list = [group]
# person.filtered_notes = sorted_personal_notes["person"].get(person.pk, [])
# prefetched_persons.append(person) if include_coursebook:
group.documentations = documentations.order_by(
# context["school_term"] = group.school_term "datetime_start"
# context["persons"] = prefetched_persons ).prefetch_related(
# context["excuse_types"] = ExcuseType.objects.filter(count_as_absent=True) prefetch_notable_participations(select_related=["person"]),
# context["excuse_types_not_absent"] = ExcuseType.objects.filter(count_as_absent=False) prefetch_personal_notes("personal_notes", select_related=["person"]),
# context["extra_marks"] = ExtraMark.objects.all() )
# context["group"] = group
# context["weeks"] = weeks context["groups"] = groups
# context["register_objects_by_day"] = register_objects_by_day
# context["register_objects"] = list(lesson_periods) + list(events) + list(extra_lessons) recorder.set_progress(3 + len(groups), _number_of_steps, _("Generating template ..."))
# context["today"] = date.today()
# context["lessons"] = ( file_object, result = generate_pdf_from_template(
# group.lessons.all() "alsijil/print/register_for_group.html",
# .select_related(None) context,
# .prefetch_related(None) file_object=PDFFile.objects.get(pk=file_object),
# .select_related("validity", "subject") )
# .prefetch_related("teachers", "lesson_periods")
# ) recorder.set_progress(4 + len(groups), _number_of_steps, _("Generating PDF ..."))
# context["child_groups"] = (
# group.child_groups.all() with allow_join_result():
# .select_related(None) result.wait()
# .prefetch_related(None) file_object.refresh_from_db()
# .prefetch_related( if not result.status == SUCCESS and file_object.file:
# "lessons", raise Exception(_("PDF generation failed"))
# "lessons__validity",
# "lessons__subject", recorder.set_progress(5 + len(groups), _number_of_steps)
# "lessons__teachers",
# "lessons__lesson_periods",
# )
# )
# recorder.set_progress(6, _number_of_steps, _("Generate template ..."))
# file_object, result = generate_pdf_from_template(
# "alsijil/print/full_register.html", context, file_object=file_object
# )
# recorder.set_progress(7, _number_of_steps, _("Generate PDF ..."))
# with allow_join_result():
# result.wait()
# file_object.refresh_from_db()
# if not result.status == SUCCESS and file_object.file:
# raise Exception(_("PDF generation failed"))
# recorder.set_progress(8, _number_of_steps)
{% load i18n rules %}
{% for note in notes %}
{% has_perm "alsijil.view_personalnote_rule" user note as can_view_personalnote %}
{% if can_view_personalnote %}
<span class="{% if note.excused %}green-text{% else %}red-text{% endif %}">{{ note.person }}
{% if note.excused %}{% if note.excuse_type %}({{ note.excuse_type.short_name }}){% else %}{% trans "(e)" %}{% endif %}{% else %}{% trans "(u)" %}{% endif %}{% if not forloop.last %},{% endif %}
</span>
{% endif %}
{% endfor %}
{% load i18n %}
<div class="card">
<div class="card-content">
<div class="card-title">{% trans "Legend" %}</div>
<div class="row">
<div class="col s12 m12 l4">
<h6>{% trans "General" %}</h6>
<ul class="collection">
<li class="collection-item chip-height">
<strong>{% trans "(a)" %}</strong> {% trans "Absences" %}
<span class="chip secondary-color white-text right">0</span>
</li>
<li class="collection-item chip-height">
<strong>{% trans "(u)" %}</strong> {% trans "Unexcused absences" %}
<span class="chip red white-text right">0</span>
</li>
<li class="collection-item chip-height">
<strong>{% trans "Sum (e)" %}</strong> {% trans "Sum of excused absences" %}
<span class="chip green white-text right">0</span>
</li>
<li class="collection-item chip-height">
<strong>{% trans "(e)" %}</strong> {% trans "Regular excused absences" %}
<span class="chip grey white-text right">0</span>
</li>
</ul>
</div>
{% if excuse_types %}
<div class="col s12 m12 l4">
<h6>{% trans "Excuse types" %}</h6>
<ul class="collection">
{% for excuse_type in excuse_types %}
<li class="collection-item chip-height">
<strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }}
<span class="chip grey white-text right">0</span>
</li>
{% endfor %}
</ul>
{% if excuse_types_not_absent %}
<h6>{% trans "Excuse types (not counted as absent)" %}</h6>
<ul class="collection">
{% for excuse_type in excuse_types_not_absent %}
<li class="collection-item chip-height">
<strong>({{ excuse_type.short_name }})</strong> {{ excuse_type.name }}
<span class="chip grey white-text right">0</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
{% if extra_marks %}
<div class="col s12 m12 l4">
<h6>{% trans "Extra marks" %}</h6>
<ul class="collection">
{% for extra_mark in extra_marks %}
<li class="collection-item chip-height">
<strong>{{ extra_mark.short_name }}</strong> {{ extra_mark.name }}
<span class="chip grey white-text right">0</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
{% load i18n week_helpers %}
{% now_datetime as now_dt %}
{% if has_documentation or register_object.has_documentation %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Data complete") icon="mdi:check-circle-outline" color="green" %}
{% elif not register_object.period %}
{% if week %}
{% period_to_time_start week register_object.raw_period_from_on_day as time_start %}
{% period_to_time_end week register_object.raw_period_to_on_day as time_end %}
{% else %}
{% period_to_time_start register_object.date_start register_object.period_from as time_start %}
{% period_to_time_end register_object.date_end register_object.period_to as time_end %}
{% endif %}
{% if now_dt > time_end %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Missing data") icon="mdi:alert-outline" color="red" %}
{% elif now_dt > time_start and now_dt < time_end %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Pending") icon="mdi:dots-horizontal" color="orange" %}
{% else %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Event") icon="mdi:calendar" color="purple" %}
{% endif %}
{% else %}
{% period_to_time_start week register_object.period as time_start %}
{% period_to_time_end week register_object.period as time_end %}
{% if substitution.cancelled or register_object.get_substitution.cancelled %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Lesson cancelled") icon="mdi:close" color="red" %}
{% elif now_dt > time_end %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Missing data") icon="mdi:alert-outline" color="red" %}
{% elif now_dt > time_start and now_dt < time_end %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Pending") icon="mdi:dots-horizontal" color="orange" %}
{% elif substitution or register_object.get_substitution %}
{% include "alsijil/partials/lesson_status_icon.html" with text=_("Substitution") icon="mdi:update" color="orange" %}
{% endif %}
{% endif %}
{% if chip %}
<span class="{% if chip %}chip{% endif %} {{ color }} white-text {{ css_class }}">
<i class="material-icons iconify left" data-icon="{{ icon }}"></i>
{{ text }}
</span>
{% else %}
<i class="material-icons iconify {{ color }}{% firstof color_suffix "-text" %} tooltipped {{ css_class }}"
data-icon="{{ icon }}"
data-position="bottom"
data-tooltip="{{ text }}" title="{{ text }}">
</i>
{% endif %}
{% load i18n material_form django_tables2 %}
<div class="card">
<div class="card-content">
<div class="card-title">{% trans "Lesson filter" %}</div>
<form action="" method="get">
{% form form=filter_form %}{% endform %}
<button type="submit" class="btn waves-effect waves-light">
<i class="material-icons iconify left" data-icon="mdi:refresh"></i>
{% trans "Update filters" %}
</button>
</form>
</div>
</div>
{% if table %}
<div class="card">
<div class="card-content">
<form action="" method="post">
{% csrf_token %}
<div class="row">
<div class="col s12 {% if action_form %}m4 l4 xl6{% endif %}">
<div class="card-title">{% trans "Lesson table" %}</div>
</div>
{% if action_form %}
<div class="col s12 m8 l8 xl6">
<div class="col s12 m8">
{% form form=action_form %}{% endform %}
</div>
<div class="col s12 m4">
<button type="submit" class="btn waves-effect waves-primary">
{% trans "Execute" %}
<i class="material-icons iconify right" data-icon="mdi:send-outline"></i>
</button>
</div>
</div>
{% endif %}
</div>
{% render_table table %}
</form>
</div>
</div>
{% endif %}
{% load static i18n data_helpers %}
<h4>{% blocktrans with full_name=person.full_name %}Personal Overview: {{ full_name }}{% endblocktrans %}</h4>
<h5>{% blocktrans %}Contact Details{% endblocktrans %}</h5>
<table class="person-info">
<tr>
<td rowspan="6" class="person-img">
{% if person.photo %}
<img src="{{ person.photo.url }}" alt="{{ person.full_name }}"/>
{% else %}
<img src="{% static 'img/fallback.png' %}" alt="{{ person.full_name }}"/>
{% endif %}
</td>
<td><i class="material-icons iconify" data-icon="mdi:account-outline"></i></td>
<td colspan="2">{{ person.first_name }} {{ person.additional_name }} {{ person.last_name }}</td>
</tr>
<tr>
<td><i class="material-icons iconify" data-icon="mdi:human-non-binary"></i></td>
<td colspan="2">{{ person.get_sex_display }}</td>
</tr>
<tr>
<td><i class="material-icons iconify" data-icon="mdi:map-marker-outline"></i></td>
<td>{{ person.street }} {{ person.housenumber }}</td>
<td>{{ person.postal_code }} {{ person.place }}</td>
</tr>
<tr>
<td><i class="material-icons iconify" data-icon="mdi:phone-outline"></i></td>
<td>{{ person.phone_number }}</td>
<td>{{ person.mobile_number }}</td>
</tr>
<tr>
<td><i class="material-icons iconify" data-icon="mdi:email-outline"></i></td>
<td colspan="2">{{ person.email }}</td>
</tr>
<tr>
<td><i class="material-icons iconify" data-icon="mdi:cake"></i></td>
<td colspan="2">{{ person.date_of_birth|date }}</td>
</tr>
</table>
<div class="row">
<div class="col s6">
<h5>{% trans 'Absences and Tardiness' %}</h5>
<table>
<tr>
<th colspan="3">{% trans 'Absences' %}</th>
<td>{{ person.absence_count }}</td>
</tr>
{% for absence_reason in absence_reasons %}
<tr style="color: {{ absence_reason.colour }};">
<th>{{ absence_reason.name }}</th>
<td>{{ person|get_dict:absence_reason.count_label }}</td>
</tr>
{% endfor %}
{% for absence_reason in absence_reasons_not_counted %}
<tr style="color: {{ absence_reason.colour }};">
<th colspan="3">{{ absence_reason.name }}</th>
<td>{{ person|get_dict:absence_reason.count_label }}</td>
</tr>
{% endfor %}
<tr>
<th colspan="3">{% trans 'Tardiness' %}</th>
<td>{{ person.tardiness_sum|default_if_none:0 }}'/{{ person.tardiness_count }}&times;</td>
</tr>
</table>
</div>
<div class="col s6">
{% if extra_marks %}
<h5>{% trans 'Extra Marks' %}</h5>
<table>
{% for extra_mark in extra_marks %}
<tr>
<th>{{ extra_mark.name }}</th>
<td>{{ person|get_dict:extra_mark.count_label }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
<h5>{% trans 'Absences and Tardinesses' %}</h5>
<table class="small-print">
<thead>
<tr>
<th>{% trans 'Date' %}</th>
<th>{% trans 'Subject' %}</th>
<th>{% trans 'Teachers' %}</th>
<th>{% trans 'Absent' %}</th>
<th>{% trans 'Tardiness' %}</th>
</tr>
</thead>
<tbody>
{% for participation in person.notable_participations %}
<tr>
<td>{{ participation.related_documentation.datetime_start }}</td>
<td>
{{ participation.related_documentation.subject.short_name }}
</td>
<td>{{ participation.related_documentation.get_teachers_short_names|join:', ' }}</td>
<td style="color: {{ absence_reason.colour }};">
{{ participation.absence_reason.short_name }}
</td>
<td>{{ participation.tardiness|default_if_none:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<h5>{% trans 'Personal Notes' %}</h5>
<table class="small-print">
<thead>
<tr>
<th>{% trans 'Date' %}</th>
<th>{% trans 'Subject' %}</th>
<th>{% trans 'Teacher' %}</th>
<th colspan="2">{% trans 'Remarks' %}</th>
</tr>
</thead>
<tbody>
{% for note in person.new_personal_notes.all %}
<tr>
<td>{{ note.documentation.datetime_start }}</td>
<td>
{{ note.documentation.subject.short_name }}
</td>
<td>{{ note.documentation.get_teachers_short_names|join:', ' }}</td>
{% if note.extra_mark %}
<td>{{ note.extra_mark.short_name }}</td>
{% endif %}
<td>{{ note.note }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% load data_helpers time_helpers i18n rules %}
{% if not persons %}
<figure class="alert primary">
<i class="material-icons iconify left" data-icon="mdi:alert-outline"></i>
{% blocktrans %}No students available.{% endblocktrans %}
</figure>
{% else %}
<table class="highlight responsive-table">
<thead>
<tr class="hide-on-med-and-down">
<th rowspan="2">{% trans "Name" %}</th>
<th rowspan="2">{% trans "Primary group" %}</th>
<th colspan="{{ excuse_types.count|add:4 }}">{% trans "Absences" %}</th>
{% if excuse_types_not_absent %}
<th colspan="{{ excuse_types_not_absent.count }}">{% trans "Uncounted Absences" %}</th>
{% endif %}
{% if extra_marks %}
<th colspan="{{ extra_marks.count }}">{% trans "Extra marks" %}</th>
{% endif %}
<th rowspan="2">{% trans "Tardiness" %}</th>
<th rowspan="2"></th>
</tr>
<tr class="hide-on-large-only">
<th class="truncate">{% trans "Name" %}</th>
<th class="truncate">{% trans "Primary group" %}</th>
<th class="truncate chip-height">{% trans "Absences" %}</th>
<th class="chip-height">{% trans "Sum (e)" %}</th>
<th class="chip-height">{% trans "(e)" %}</th>
{% for excuse_type in excuse_types %}
<th class="chip-height">
({{ excuse_type.short_name }})
</th>
{% endfor %}
<th class="chip-height">{% trans "(u)" %}</th>
{% for excuse_type in excuse_types_not_absent %}
<th class="chip-height">
({{ excuse_type.short_name }})
</th>
{% endfor %}
{% for extra_mark in extra_marks %}
<th class="chip-height">
{{ extra_mark.short_name }}
</th>
{% endfor %}
<th class="truncate chip-height">{% trans "Tardiness" %}</th>
<th rowspan="2"></th>
</tr>
<tr class="hide-on-med-and-down">
<th>{% trans "Sum" %}</th>
<th>{% trans "Sum (e)" %}</th>
<th>{% trans "(e)" %}</th>
{% for excuse_type in excuse_types %}
<th>
({{ excuse_type.short_name }})
</th>
{% endfor %}
<th>{% trans "(u)" %}</th>
{% for excuse_type in excuse_types_not_absent %}
<th>
({{ excuse_type.short_name }})
</th>
{% endfor %}
{% for extra_mark in extra_marks %}
<th>
{{ extra_mark.short_name }}
</th>
{% endfor %}
</tr>
</thead>
{% for person in persons %}
<tr>
<td>
<a href="{% url "overview_person" person.pk %}">
{{ person }}
</a>
</td>
<td>
{% firstof person.primary_group "–" %}
</td>
<td>
<span class="chip secondary-color white-text" title="{% trans "Absences" %}">
{{ person.absences_count }}
</span>
</td>
<td class="green-text">
<span class="chip green white-text" title="{% trans "Excused" %}">
{{ person.excused }}
</span>
</td>
<td>
<span class="chip grey white-text" title="{% trans "Regular excused" %}">
{{ person.excused_without_excuse_type }}
</span>
</td>
{% for excuse_type in excuse_types %}
<td>
<span class="chip grey white-text" title="{{ excuse_type.name }}">
{{ person|get_dict:excuse_type.count_label }}
</span>
</td>
{% endfor %}
<td class="red-text">
<span class="chip red white-text" title="{% trans "Unexcused" %}">
{{ person.unexcused }}
</span>
</td>
{% for excuse_type in excuse_types_not_absent %}
<td>
<span class="chip grey white-text" title="{{ excuse_type.name }}">
{{ person|get_dict:excuse_type.count_label }}
</span>
</td>
{% endfor %}
{% for extra_mark in extra_marks %}
<td>
<span class="chip grey white-text" title="{{ extra_mark.name }}">
{{ person|get_dict:extra_mark.count_label }}
</span>
</td>
{% endfor %}
<td>
<span class="chip orange white-text" title="{% trans "Tardiness" %}">
{% firstof person.tardiness|to_time|time:"H\h i\m" "–" %}
</span>
<span class="chip orange white-text" title="{% trans "Count of tardiness" %}">{{ person.tardiness_count }} &times;</span>
</td>
<td>
<a class="btn primary waves-effect waves-light" href="{% url "overview_person" person.pk %}">
<i class="material-icons iconify left" data-icon="mdi:chart-box-outline"></i>
<span class="hide-on-med-and-down"> {% trans "Show more details" %}</span>
<span class="hide-on-large-only">{% trans "Details" %}</span>
</a>
{% has_perm "alsijil.register_absence_rule" user person as can_register_absence %}
{% if can_register_absence %}
<a class="btn primary-color waves-effect waves-light" href="{% url "register_absence" person.pk %}">
<i class="material-icons iconify left" data-icon="mdi:message-draw"></i>
{% trans "Register absence" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% load i18n %}
<h4>{% trans "Abbreviations" %}</h4>
<h5>{% trans "General" %}</h5>
<!-- TODO: This implies AbsenceReasons can not have the shortNames a and b! -->
<ul class="collection">
<li class="collection-item">
<strong>(a)</strong> {% trans "Absent" %}
</li>
<li class="collection-item">
<strong>(b)</strong> {% trans "Late" %}
</li>
</ul>
{% if absence_reasons %}
<h5>{% trans "Absence Reasons" %}</h5>
<ul class="collection">
{% for absence_reason in absence_reasons %}
<li class="collection-item" style="color: {{ absence_reason.colour }};">
<strong>({{ absence_reason.short_name }})</strong> {{ absence_reason.name }}
</li>
{% endfor %}
{% for absence_reason in absence_reasons_not_counted %}
<li class="collection-item" style="color: {{ absence_reason.colour }};">
<strong>({{ absence_reason.short_name }})</strong> {{ absence_reason.name }}
</li>
{% endfor %}
</ul>
{% endif %}
{% if extra_marks %}
<h5>{% trans "Extra Marks" %}</h5>
<ul class="collection">
{% for extra_mark in extra_marks %}
<li class="collection-item">
<strong>({{ extra_mark.short_name }})</strong> {{ extra_mark.name }}
</li>
{% endfor %}
</ul>
{% endif %}
{% load i18n %}
<h4>{% trans 'Coursebook' %}</h4>
<table class="small-print">
<thead>
<tr>
<th>{% trans 'Time' %}</th>
<th>{% trans 'Subj.' %}</th>
<th>{% trans 'Topic' %}</th>
<th>{% trans 'Homework' %}</th>
<th>{% trans 'Notes' %}</th>
<th>{% trans 'Te.' %}</th>
</tr>
</thead>
<tbody>
{% for doc in group.documentations %}
{% ifchanged doc.datetime_start.date %}
<tr><th colspan="6">{{ doc.datetime_start.date|date:"D d M Y" }}</th></tr>
{% endifchanged %}
<tr class="
{% if doc.amends %}
{% if doc.amends.cancelled %}
lesson-cancelled
{% endif %}
{% if doc.amends.amends %}
lesson-substituted
{% endif %}
{% endif %}
{% ifchanged doc.datetime_start.date %}
lessons-day-first
{% endifchanged %}
">
<td class="lesson-pe">
{% if doc.amends %}
{% if doc.amends.slot_number_start == doc.amends.slot_number_end %}
{{ doc.amends.slot_number_start }}.
{% else %}
{{ doc.amends.slot_number_start }}.–{{ doc.amends.slot_number_end }}.
{% endif %}
{% else %}
{{ doc.datetime_start|time:"H:i" }}-{{ doc.datetime_end|time:"H:i" }}
{% endif %}
</td>
<td class="lesson-subj">
{% if doc.subject %}
{{ doc.subject.short_name|default:doc.subject.name }}
{% endif %}
</td>
<td class="lesson-topic">
{{ doc.topic }}
</td>
<td class="lesson-homework">{{ doc.homework }}</td>
<td class="lesson-notes">
{{ documentation.group_note }}
{% for participation in doc.notable_participations %}
{% if participation.absence_reason %}
<span class="lesson-note-absent">
{{ participation.person.full_name }}
<span style="color: {{ participation.absence_reason.colour }};">
({{ participation.absence_reason.short_name }})
</span>
</span>
{% endif %}
{% if participation.tardiness %}
<span class="lesson-note-late">
{{ participation.person.full_name }}
({{ participation.tardiness }}′)
</span>
{% endif %}
{% endfor %}
{% for personal_note in doc.personal_notes.all %}
{% if personal_note.extra_mark %}
<span>
{{ personal_note.person.full_name }}
({{ personal_note.extra_mark.short_name }})
</span>
{% endif %}
{% if personal_note.note %}
<span>
{{ personal_note.person.full_name }}
({{ personal_note.note }})
</span>
{% endif %}
{% endfor %}
</td>
<td class="lesson-te">
{% if doc.topic %}
{{ doc.get_teachers_short_names|join:', ' }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment