Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • AlekSIS/official/AlekSIS-App-Alsijil
  • sunweaver/AlekSIS-App-Alsijil
  • 8tincsoVluke/AlekSIS-App-Alsijil
  • perfreicpo/AlekSIS-App-Alsijil
  • noifobarep/AlekSIS-App-Alsijil
  • 7ingannisdo/AlekSIS-App-Alsijil
  • unmruntartpa/AlekSIS-App-Alsijil
  • balrorebta/AlekSIS-App-Alsijil
  • comliFdifwa/AlekSIS-App-Alsijil
  • 3ranaadza/AlekSIS-App-Alsijil
10 results
Show changes
Showing
with 1149 additions and 304 deletions
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load data_helpers %}
{% load week_helpers %}
{% load i18n %}
{% load i18n week_helpers data_helpers static time_helpers %}
{% block browser_title %}{% blocktrans %}My students{% endblocktrans %}{% endblock %}
......@@ -11,16 +9,56 @@
{% blocktrans %}My students{% endblocktrans %}
{% endblock %}
{% block extra_head %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/>
{% endblock %}
{% block content %}
<div class="collection">
{% for person in persons %}
<a class="collection-item" href="{% url "overview_person" person.pk %}">
{{ person }}
</a>
{% empty %}
<li class="collection-item flow-text">
{% blocktrans %}No students available.{% endblocktrans %}
<ul class="collapsible">
{% for group, persons in groups %}
<li {% if forloop.first %}class="active"{% endif %}>
<div class="collapsible-header">
<div class="hundred-percent">
<span class="right show-on-active hide-on-small-and-down">
<a class="btn primary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}">
<i class="material-icons left">view_week</i>
{% trans "Week view" %}
</a>
<a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</span>
<h6>{{ group.name }}
<span class="chip">{{ group.school_term }}</span>
</h6>
<p class="show-on-active hide-on-med-and-up">
<a class="btn primary-color waves-effect waves-light hundred-percent"
href="{% url "week_view" "group" group.pk %}">
<i class="material-icons left">view_week</i>
{% trans "Week view" %}
</a>
</p>
<p class="show-on-active hide-on-med-and-up">
<a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}"
target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</p>
</div>
</div>
<div class="collapsible-body">
{% include "alsijil/partials/persons_with_stats.html" with persons=persons %}
</div>
</li>
{% endfor %}
</div>
</ul>
{% include "alsijil/partials/legend.html" %}
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load static time_helpers data_helpers week_helpers i18n %}
{% block browser_title %}{% blocktrans with group=group %}Students list: {{ group }}{% endblocktrans %}{% endblock %}
{% block page_title %}
<a href="{% url "my_groups" %}"
class="btn-flat primary-color-text waves-light waves-effect">
<i class="material-icons left">chevron_left</i> {% trans "Back" %}
</a>
{% blocktrans with group=group %}Students list: {{ group }}{% endblocktrans %}
<span class="right show-on-active hide-on-small-and-down">
<a class="btn primary-color waves-effect waves-light" href="{% url "week_view" "group" group.pk %}">
<i class="material-icons left">view_week</i>
{% trans "Week view" %}
</a>
<a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</span>
{% endblock %}
{% block extra_head %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/alsijil/alsijil.css' %}"/>
{% endblock %}
{% block content %}
<p class="show-on-active hide-on-med-and-up">
<a class="btn primary-color waves-effect waves-light hundred-percent"
href="{% url "week_view" "group" group.pk %}">
<i class="material-icons left">view_week</i>
{% trans "Week view" %}
</a>
</p>
<p class="show-on-active hide-on-med-and-up">
<a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}"
target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</p>
{% include "alsijil/partials/persons_with_stats.html" with persons=persons %}
{% include "alsijil/partials/legend.html" %}
{% endblock %}
{# -*- engine:django -*- #}
{% extends "core/base.html" %}
{% load material_form i18n week_helpers static data_helpers %}
{% load material_form i18n week_helpers static data_helpers rules time_helpers %}
{% block browser_title %}{% blocktrans %}Week view{% endblocktrans %}{% endblock %}
......@@ -15,14 +15,7 @@
{{ week_select|json_script:"week_select" }}
<script type="text/javascript" src="{% static "js/chronos/week_select.js" %}"></script>
<div class="row">
{% if group %}
<div class="col s12 m2 push-m10 l1 push-l11">
<a class="col s12 btn waves-effect waves-light right" href="{% url 'full_register_group' group.id %}">
<i class="material-icons center">print</i>
</a>
</div>
{% endif %}
<div class="col s12 {% if group %}m10 pull-m2 l11 pull-l1 {% endif %}">
<div class="col s12">
<form method="post" action="">
{% csrf_token %}
{% form form=select_form %}{% endform %}
......@@ -34,18 +27,51 @@
</div>
<div class="row">
<div class="row no-margin">
<h4 class="col s12 m6">{% blocktrans with el=el week=week.week %}CW {{ week }}:
{{ instance }}{% endblocktrans %} </h4>
{% include "chronos/partials/week_select.html" with wanted_week=week %}
</div>
{% if group %}
<p class="hide-on-med-and-down">
<a class="btn primary-color waves-effect waves-light" href="{% url "students_list" group.pk %}">
<i class="material-icons left">people</i>
{% trans "Students list" %}
</a>
<a class="btn waves-effect waves-light" href="{% url "full_register_group" group.pk %}" target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</p>
<p class="hide-on-med-and-up">
<a class="btn primary-color waves-effect waves-light hundred-percent" href="{% url "students_list" group.pk %}">
<i class="material-icons left">people</i>
{% trans "Students list" %}
</a>
</p>
<p class="hide-on-med-and-up">
<a class="btn waves-effect waves-light hundred-percent" href="{% url "full_register_group" group.pk %}"
target="_blank">
<i class="material-icons left">print</i>
{% trans "Generate printout" %}
</a>
</p>
{% endif %}
{% if lesson_periods %}
<div class="row">
<div class="col s12 m7">
<div class="col s12">
<ul class="tabs">
<li class="tab col s6"><a class="active" href="#week-overview">{% trans "Lesson documentations" %}</a></li>
<li class="tab col s6"><a class="active" href="#personal-notes">{% trans "Personal notes" %}</a></li>
</ul>
</div>
<div class="col s12" id="week-overview">
{% regroup lesson_periods by period.get_weekday_display as periods_by_day %}
{% for weekday, periods in periods_by_day %}
<div class="card">
<div class="card show-on-extra-large">
<div class="card-content">
{% weekday_to_date week periods.0.period.weekday as current_date %}
<span class="card-title">
......@@ -62,50 +88,154 @@
<th>{% blocktrans %}Subject{% endblocktrans %}</th>
<th>{% blocktrans %}Teachers{% endblocktrans %}</th>
<th>{% blocktrans %}Lesson topic{% endblocktrans %}</th>
<th>{% blocktrans %}Homework{% endblocktrans %}</th>
<th>{% blocktrans %}Group note{% endblocktrans %}</th>
</tr>
</thead>
<tbody>
{% for period in periods %}
<tr>
<td class="center-align">
{% include "alsijil/partials/lesson_status_icon.html" with period=period %}
</td>
<td class="tr-link">
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.period.period }}.
</a>
</td>
{% if not group %}
{% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %}
{% if can_view_lesson_documentation %}
<tr>
<td class="center-align">
{% include "alsijil/partials/lesson_status_icon.html" with period=period %}
</td>
<td class="tr-link">
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.period.period }}.
</a>
</td>
{% if not group %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.lesson.group_names }}
</a>
</td>
{% endif %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.lesson.group_names }}
{{ period.get_subject.name }}
</a>
</td>
{% endif %}
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_subject.name }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_teacher_names }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_lesson_documentation.topic }}
</a>
</td>
</tr>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{{ period.get_teacher_names }}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{% firstof period.get_lesson_documentation.topic "–" %}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{% firstof period.get_lesson_documentation.homework "–" %}
</a>
</td>
<td>
<a class="tr-link" href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{% firstof period.get_lesson_documentation.group_note "–" %}
</a>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<ul class="collapsible hide-on-extra-large-only">
<li class="">
{% weekday_to_date week periods.0.period.weekday as current_date %}
<div class="collapsible-header flow-text">
{{ weekday }}, {{ current_date }} <i class="material-icons collapsible-icon-right">expand_more</i>
</div>
<div class="collapsible-body">
<div class="collection">
{% for period in periods %}
{% has_perm "alsijil.view_lessondocumentation" user period as can_view_lesson_documentation %}
{% if can_view_lesson_documentation %}
<a class="collection-item avatar"
href="{% url 'lesson_by_week_and_period' week.year week.week period.id %}">
{% include "alsijil/partials/lesson_status_icon.html" with period=period css_class="circle" color_suffix=" " %}
<table class="hide-on-med-and-down">
<tr>
<th>{% trans "Subject" %}</th>
<td>{{ period.period.period }}. {{ period.get_subject.name }}</td>
</tr>
{% if not group %}
<tr>
<th>{% trans "Group" %}</th>
<td>{{ period.lesson.group_names }}</td>
</tr>
{% endif %}
<tr>
<th>{% trans "Teachers" %}</th>
<td>{{ period.lesson.teacher_names }}</td>
</tr>
<tr>
<th>{% trans "Lesson topic" %}</th>
<td>{% firstof period.get_lesson_documentation.topic "–" %}</td>
</tr>
{% with period.get_lesson_documentation as lesson_documentation %}
{% if lesson_documentation.homework %}
<tr>
<th>{% trans "Homework" %}</th>
<td>{% firstof period.get_lesson_documentation.homework "–" %}</td>
</tr>
{% endif %}
{% if lesson_documentation.group_note %}
<tr>
<th>{% trans "Group note" %}</th>
<td>{% firstof period.get_lesson_documentation.group_note "–" %}</td>
</tr>
{% endif %}
{% endwith %}
</table>
<div class="hide-on-large-only">
<ul class="collection">
<li class="collection-item">
{{ period.period.period }}. {{ period.get_subject.name }}
</li>
{% if not group %}
<li class="collection-item">
{{ period.lesson.group_names }}
</li>
{% endif %}
<li class="collection-item">
{{ period.lesson.teacher_names }}
</li>
<li class="collection-item">
{{ period.get_lesson_documentation.topic }}
</li>
{% with period.get_lesson_documentation as lesson_documentation %}
{% if lesson_documentation.homework %}
<li class="collection-item">
<strong>{% trans "Homework" %}</strong>
{% firstof period.get_lesson_documentation.homework "–" %}
</li>
{% endif %}
{% if lesson_documentation.group_note %}
<li class="collection-item">
<strong>{% trans "Group note" %}</strong>
{% firstof period.get_lesson_documentation.group_note "–" %}
</li>
{% endif %}
{% endwith %}
</ul>
</div>
</a>
{% endif %}
{% endfor %}
</div>
</div>
</li>
</ul>
{% endfor %}
</div>
<div class="col s12 m5">
<div class="col s12" id="personal-notes">
<div class="card">
<div class="card-content">
<span class="card-title">
......@@ -113,14 +243,29 @@
</span>
{% for person in persons %}
<h5 class="card-title">
<a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a>
{% has_perm "alsijil.view_person_overview" user person.person as can_view_person_overview %}
{% if can_view_person_overview %}
<a href="{% url "overview_person" person.person.pk %}">{{ person.person.full_name }}</a>
{% else %}
{{ person.person.full_name }}
{% endif %}
{% has_perm "alsijil.register_absence" user person.person as can_register_absence %}
{% if can_register_absence %}
<a class="btn primary-color waves-effect waves-light right" href="{% url "register_absence" person.person.pk %}">
<i class="material-icons left">rate_review</i>
{% trans "Register absence" %}
</a>
{% endif %}
</h5>
<p class="card-text">
{% trans "Absent" %}: {{ person.person.absences_count }}
({{ person.person.unexcused_count }} {% trans "unexcused" %})
</p>
<p class="card-text">
{% trans "Summed up tardiness" %}: {{ person.person.tardiness_sum }}'
{% trans "Summed up tardiness" %}: {% firstof person.person.tardiness_sum|to_time|time:"H\h i\m" "–" %}
</p>
<p class="card-text">
{% trans "Count of tardiness" %}: {{ person.person.tardiness_count }} &times;
</p>
{% for extra_mark in extra_marks %}
<p class="card-text">
......
......@@ -2,7 +2,7 @@
{% extends "core/base.html" %}
{% load i18n %}
{% load i18n rules %}
{% load render_table from django_tables2 %}
{% block browser_title %}{% blocktrans %}Excuse types{% endblocktrans %}{% endblock %}
......@@ -11,10 +11,13 @@
{% block content %}
{% include "alsijil/excuse_type/warning.html" %}
<a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}">
<i class="material-icons left">add</i>
{% trans "Create excuse type" %}
</a>
{% has_perm "alsijil.add_excusetype" user as add_excusetype %}
{% if add_excusetype %}
<a class="btn green waves-effect waves-light" href="{% url 'create_excuse_type' %}">
<i class="material-icons left">add</i>
{% trans "Create excuse type" %}
</a>
{% endif %}
{% render_table table %}
{% endblock %}
{% load i18n %}
{% load i18n rules %}
{% for note in notes %}
<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>
{% has_perm "alsijil.view_personalnote" 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>(a)</strong> {% trans "Absences" %}
<span class="chip secondary-color white-text right">0</span>
</li>
<li class="collection-item chip-height">
<strong>(u)</strong> {% trans "Unexcused absences" %}
<span class="chip red white-text right">0</span>
</li>
<li class="collection-item chip-height">
<strong>(e)</strong> {% trans "Excused absences" %}
<span class="chip green 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>
</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>
......@@ -3,18 +3,18 @@
{% now_datetime as now_dt %}
{% if period.has_documentation %}
<i class="material-icons green-text tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Data complete" %}" title="{% trans "Data complete" %}">check_circle</i>
<i class="material-icons green{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Data complete" %}" title="{% trans "Data complete" %}">check_circle</i>
{% else %}
{% period_to_time_start week period.period as time_start %}
{% period_to_time_end week period.period as time_end %}
{% if period.get_substitution.cancelled %}
<i class="material-icons red-text tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Lesson cancelled" %}" title="{% trans "Lesson cancelled" %}">cancel</i>
<i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Lesson cancelled" %}" title="{% trans "Lesson cancelled" %}">cancel</i>
{% elif now_dt > time_end %}
<i class="material-icons red-text tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Missing data" %}" title="{% trans "Missing data" %}">history</i>
<i class="material-icons red{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Missing data" %}" title="{% trans "Missing data" %}">history</i>
{% elif now_dt > time_start and now_dt < time_end %}
<i class="material-icons orange-text tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Pending" %}" title="{% trans "Pending" %}">more_horiz</i>
<i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Pending" %}" title="{% trans "Pending" %}">more_horiz</i>
{% elif period.get_substitution %}
<i class="material-icons orange-text tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Substitution" %}" title="{% trans "Substitution" %}">update</i>
<i class="material-icons orange{% firstof color_suffix "-text"%} tooltipped {{ css_class }}" data-position="bottom" data-tooltip="{% trans "Substitution" %}" title="{% trans "Substitution" %}">update</i>
{% endif %}
{% endif %}
{% load data_helpers time_helpers i18n rules %}
{% if not persons %}
<div class="alert primary">
<div>
<i class="material-icons left">warning</i>
{% blocktrans %}No students available.{% endblocktrans %}
</div>
</div>
{% 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:3 }}">{% trans "Absences" %}</th>
<th rowspan="2">{% trans "Tardiness" %}</th>
{% if extra_marks %}
<th colspan="{{ extra_marks.count }}">{% trans "Extra marks" %}</th>
{% endif %}
<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 "(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>
<th class="truncate chip-height">{% trans "Tardiness" %}</th>
{% for extra_mark in extra_marks %}
<th class="chip-height">
{{ extra_mark.short_name }}
</th>
{% endfor %}
<th rowspan="2"></th>
</tr>
<tr class="hide-on-med-and-down">
<th>{% trans "Sum" %}</th>
<th>{% trans "(e)" %}</th>
{% for excuse_type in excuse_types %}
<th>
({{ excuse_type.short_name }})
</th>
{% endfor %}
<th>{% trans "(u)" %}</th>
{% 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>
{% 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>
<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>
{% 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>
<a class="btn primary waves-effect waves-light" href="{% url "overview_person" person.pk %}">
<i class="material-icons left">insert_chart</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" 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 left">rate_review</i>
{% trans "Register absence" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% load rules %}
{% for note in notes %}
<span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span>
{% has_perm "alsijil.view_personalnote" user note as can_view_personalnote %}
{% if can_view_personalnote %}
<span>{{ note.person }} ({{ note.late }}'){% if not forloop.last %},{% endif %}</span>
{% endif %}
{% endfor %}
......@@ -149,7 +149,7 @@
<td>{{ person|get_dict:excuse_type.count_label }}</td>
{% endfor %}
<td>{{ person.unexcused }}</td>
<td>{{ person.tardiness }}'</td>
<td>{{ person.tardiness }}'/{{ person.tardiness_count }} &times;</td>
{% for extra_mark in extra_marks %}
<td>{{ person|get_dict:extra_mark.count_label }}</td>
{% endfor %}
......@@ -175,7 +175,7 @@
</thead>
<tbody>
{% for lesson in group.lessons.all %}
{% for lesson in lessons %}
<tr>
<td>{{ lesson.subject.name }}</td>
<td>{{ lesson.teachers.all|join:', ' }}</td>
......@@ -206,7 +206,7 @@
</thead>
<tbody>
{% for child_group in group.child_groups.all %}
{% for child_group in child_groups %}
{% for lesson in child_group.lessons.all %}
<tr>
<td>{{ child_group.name }}</td>
......@@ -285,7 +285,7 @@
</tr>
<tr>
<th colspan="2">{% trans 'Tardiness' %}</th>
<td>{{ person.tardiness }}'</td>
<td>{{ person.tardiness }}'/{{ person.tardiness_count }} &times;</td>
</tr>
</table>
......
import datetime
from django import template
register = template.Library()
@register.filter("to_time")
def get_time_from_minutes(minutes: int) -> datetime.timedelta:
"""Get a time object from a number of minutes."""
delta = datetime.timedelta(minutes=(minutes or 0))
time_obj = (datetime.datetime.min + delta).time()
return time_obj
......@@ -27,6 +27,7 @@ urlpatterns = [
"print/group/<int:id_>", views.full_register_group, name="full_register_group"
),
path("groups/", views.my_groups, name="my_groups"),
path("groups/<int:pk>/", views.StudentsList.as_view(), name="students_list"),
path("persons/", views.my_students, name="my_students"),
path("persons/<int:id_>/", views.overview_person, name="overview_person"),
path("me/", views.overview_person, name="overview_me"),
......@@ -35,7 +36,7 @@ urlpatterns = [
views.DeletePersonalNoteView.as_view(),
name="delete_personal_note",
),
path("absence/new", views.register_absence, name="register_absence"),
path("absence/new/<int:id_>/", views.register_absence, name="register_absence"),
path("extra_marks/", views.ExtraMarkListView.as_view(), name="extra_marks"),
path(
"extra_marks/create/",
......
from typing import Optional
from django.http import HttpRequest
from calendarweek import CalendarWeek
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
def get_lesson_period_by_pk(
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
period_id: Optional[int] = None,
):
"""Get LessonPeriod object either by given object_id or by time and current person."""
wanted_week = CalendarWeek(year=year, week=week)
if period_id:
lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(
pk=period_id
)
elif hasattr(request, "user") and hasattr(request.user, "person"):
if request.user.person.lessons_as_teacher.exists():
lesson_period = (
LessonPeriod.objects.at_time()
.filter_teacher(request.user.person)
.first()
)
else:
lesson_period = (
LessonPeriod.objects.at_time()
.filter_participant(request.user.person)
.first()
)
else:
lesson_period = None
return lesson_period
def get_timetable_instance_by_pk(
request: HttpRequest,
year: Optional[int] = None,
week: Optional[int] = None,
type_: Optional[str] = None,
id_: Optional[int] = None,
):
"""Get timetable object (teacher, room or group) by given type and id or the current person."""
if type_ and id_:
return get_el_by_pk(request, type_, id_)
elif hasattr(request, "user") and hasattr(request.user, "person"):
return request.user.person
from typing import Any, Union
from django.contrib.auth.models import Permission, User
from guardian.models import UserObjectPermission
from guardian.shortcuts import get_objects_for_user
from rules import predicate
from aleksis.apps.chronos.models import LessonPeriod
from aleksis.core.models import Group, Person
from aleksis.core.util.core_helpers import get_content_type_by_perm, get_site_preferences
from aleksis.core.util.predicates import check_object_permission
from ..models import PersonalNote
@predicate
def is_none(user: User, obj: Any) -> bool:
"""Predicate that checks if the provided object is None-like."""
return bool(obj)
@predicate
def is_lesson_teacher(user: User, obj: LessonPeriod) -> bool:
"""Predicate for teachers of a lesson.
Checks whether the person linked to the user is a teacher
in the lesson or the substitution linked to the given LessonPeriod.
"""
if obj:
sub = obj.get_substitution()
if sub and sub in user.person.lesson_substitutions.all():
return True
return user.person in obj.lesson.teachers.all()
return False
@predicate
def is_lesson_participant(user: User, obj: LessonPeriod) -> bool:
"""Predicate for participants of a lesson.
Checks whether the person linked to the user is a member in
the groups linked to the given LessonPeriod.
"""
if hasattr(obj, "lesson"):
for group in obj.lesson.groups.all():
if user.person in list(group.members.all()):
return True
return False
@predicate
def is_lesson_parent_group_owner(user: User, obj: LessonPeriod) -> bool:
"""
Predicate for parent group owners of a lesson.
Checks whether the person linked to the user is the owner of
any parent groups of any groups of the given LessonPeriods lesson.
"""
if hasattr(obj, "lesson"):
for group in obj.lesson.groups.all():
for parent_group in group.parent_groups.all():
if user.person in list(parent_group.owners.all()):
return True
return False
@predicate
def is_group_owner(user: User, obj: Union[Group, Person]) -> bool:
"""Predicate for group owners of a given group.
Checks whether the person linked to the user is the owner of the given group.
If there isn't provided a group, it will return `False`.
"""
if isinstance(obj, Group):
if user.person in obj.owners.all():
return True
return False
@predicate
def is_person_group_owner(user: User, obj: Person) -> bool:
"""
Predicate for group owners of any group.
Checks whether the person linked to the user is
the owner of any group of the given person.
"""
if obj:
for group in obj.member_of.all():
if user.person in list(group.owners.all()):
return True
return False
return False
@predicate
def is_person_primary_group_owner(user: User, obj: Person) -> bool:
"""
Predicate for group owners of the person's primary group.
Checks whether the person linked to the user is
the owner of the primary group of the given person.
"""
if obj.primary_group:
return user.person in obj.primary_group.owners.all()
return False
def has_person_group_object_perm(perm: str):
"""Predicate builder for permissions on a set of member groups.
Checks whether a user has a permission on any group of a person.
"""
name = f"has_person_group_object_perm:{perm}"
ct = get_content_type_by_perm(perm)
permissions = Permission.objects.filter(content_type=ct, codename=perm)
@predicate(name)
def fn(user: User, obj: Person) -> bool:
groups = obj.member_of.all()
qs = UserObjectPermission.objects.filter(
object_pk__in=list(groups.values_list("pk", flat=True)),
content_type=ct,
user=user,
permission__in=permissions,
)
return qs.exists()
return fn
@predicate
def is_group_member(user: User, obj: Union[Group, Person]) -> bool:
"""Predicate for group membership.
Checks whether the person linked to the user is a member of the given group.
If there isn't provided a group, it will return `False`.
"""
if isinstance(obj, Group):
if user.person in obj.members.all():
return True
return False
def has_lesson_group_object_perm(perm: str):
"""Predicate builder for permissions on lesson groups.
Checks whether a user has a permission on any group of a LessonPeriod.
"""
name = f"has_lesson_group_object_perm:{perm}"
ct = get_content_type_by_perm(perm)
permissions = Permission.objects.filter(content_type=ct, codename=perm)
@predicate(name)
def fn(user: User, obj: LessonPeriod) -> bool:
if hasattr(obj, "lesson"):
groups = obj.lesson.groups.all()
qs = UserObjectPermission.objects.filter(
object_pk__in=list(groups.values_list("pk", flat=True)),
content_type=ct,
user=user,
permission__in=permissions,
)
return qs.exists()
return False
return fn
def has_personal_note_group_perm(perm: str):
"""Predicate builder for permissions on personal notes
Checks whether a user has a permission on any group of a person of a PersonalNote.
"""
name = f"has_personal_note_person_or_group_perm:{perm}"
ct = get_content_type_by_perm(perm)
permissions = Permission.objects.filter(content_type=ct, codename=perm)
@predicate(name)
def fn(user: User, obj: PersonalNote) -> bool:
if hasattr(obj, "person"):
groups = obj.person.member_of.all()
qs = UserObjectPermission.objects.filter(
object_pk__in=list(groups.values_list("pk", flat=True)),
content_type=ct,
user=user,
permission__in=permissions,
)
return qs.exists()
return False
return fn
@predicate
def is_own_personal_note(user: User, obj: PersonalNote) -> bool:
"""Predicate for users referred to in a personal note
Checks whether the user referred to in a PersonalNote is the active user.
"""
if hasattr(obj, "person") and obj.person is user.person:
return True
return False
@predicate
def is_personal_note_lesson_teacher(user: User, obj: PersonalNote) -> bool:
"""Predicate for teachers of a lesson referred to in the lesson period of a personal note.
Checks whether the person linked to the user is a teacher
in the lesson or the substitution linked to the LessonPeriod of the given PersonalNote.
"""
if hasattr(obj, "lesson_period"):
if hasattr(obj.lesson_period, "lesson"):
sub = obj.lesson_period.get_substitution()
if sub and user.person in Person.objects.filter(
lesson_substitutions=obj.lesson_period.get_substitution()
):
return True
return user.person in obj.lesson_period.lesson.teachers.all()
return False
return False
@predicate
def is_personal_note_lesson_parent_group_owner(user: User, obj: PersonalNote) -> bool:
"""
Predicate for parent group owners of a lesson referred to in the lesson period of a personal note.
Checks whether the person linked to the user is the owner of
any parent groups of any groups of the given LessonPeriod lesson of the given PersonalNote.
"""
if hasattr(obj, "lesson_period"):
if hasattr(obj.lesson_period, "lesson"):
for group in obj.lesson_period.lesson.groups.all():
for parent_group in group.parent_groups.all():
if user.person in list(parent_group.owners.all()):
return True
return False
@predicate
def has_any_object_absence(user: User) -> bool:
"""
Predicate which builds a query with all the persons the given users is allowed to register an absence for.
"""
if Person.objects.filter(member_of__owners=user.person).exists():
return True
if get_objects_for_user(user, "core.register_absence_person", Person).exists():
return True
if Person.objects.filter(
member_of__in=get_objects_for_user(user, "core.register_absence_group", Group)
).exists():
return True
@predicate
def is_teacher(user: User, obj: Person) -> bool:
"""Predicate which checks if the provided object is a teacher."""
return user.person.is_teacher
......@@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta
from typing import Optional
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Exists, OuterRef, Q, Subquery, Sum
from django.db.models import Count, Exists, OuterRef, Prefetch, Q, Subquery, Sum
from django.http import Http404, HttpRequest, HttpResponse, HttpResponseNotFound
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
......@@ -13,10 +13,10 @@ import reversion
from calendarweek import CalendarWeek
from django_tables2 import SingleTableView
from reversion.views import RevisionMixin
from rules.contrib.views import PermissionRequiredMixin
from rules.contrib.views import PermissionRequiredMixin, permission_required
from aleksis.apps.chronos.managers import TimetableType
from aleksis.apps.chronos.models import LessonPeriod, TimePeriod
from aleksis.apps.chronos.models import LessonPeriod, LessonSubstitution, TimePeriod
from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
from aleksis.apps.chronos.util.date import get_weeks_for_year, week_weekday_to_date
from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView
......@@ -34,8 +34,10 @@ from .forms import (
)
from .models import ExcuseType, ExtraMark, LessonDocumentation, PersonalNote
from .tables import ExcuseTypeTable, ExtraMarkTable
from .util.alsijil_helpers import get_lesson_period_by_pk, get_timetable_instance_by_pk
@permission_required("alsijil.view_lesson", fn=get_lesson_period_by_pk)
def lesson(
request: HttpRequest,
year: Optional[int] = None,
......@@ -44,27 +46,16 @@ def lesson(
) -> HttpResponse:
context = {}
if year and week and period_id:
# Get a specific lesson period if provided in URL
wanted_week = CalendarWeek(year=year, week=week)
lesson_period = LessonPeriod.objects.annotate_week(wanted_week).get(
pk=period_id
)
lesson_period = get_lesson_period_by_pk(request, year, week, period_id)
date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
if (
date_of_lesson < lesson_period.lesson.validity.date_start
or date_of_lesson > lesson_period.lesson.validity.date_end
):
return HttpResponseNotFound()
else:
# Determine current lesson by current date and time
lesson_period = (
LessonPeriod.objects.at_time().filter_teacher(request.user.person).first()
)
if period_id:
wanted_week = CalendarWeek(year=year, week=week)
elif hasattr(request, "user") and hasattr(request.user, "person"):
wanted_week = CalendarWeek()
else:
wanted_week = None
if not all((year, week, period_id)):
if lesson_period:
return redirect(
"lesson_by_week_and_period",
......@@ -80,6 +71,14 @@ def lesson(
)
)
date_of_lesson = week_weekday_to_date(wanted_week, lesson_period.period.weekday)
if (
date_of_lesson < lesson_period.lesson.validity.date_start
or date_of_lesson > lesson_period.lesson.validity.date_end
):
return HttpResponseNotFound()
if (
datetime.combine(
wanted_week[lesson_period.period.weekday], lesson_period.period.time_start,
......@@ -97,9 +96,14 @@ def lesson(
)
)
next_lesson = request.user.person.next_lesson(lesson_period, date_of_lesson)
prev_lesson = request.user.person.previous_lesson(lesson_period, date_of_lesson)
context["lesson_period"] = lesson_period
context["week"] = wanted_week
context["day"] = wanted_week[lesson_period.period.weekday]
context["next_lesson_person"] = next_lesson
context["prev_lesson_person"] = prev_lesson
# Create or get lesson documentation object; can be empty when first opening lesson
lesson_documentation = lesson_period.get_or_create_lesson_documentation(wanted_week)
......@@ -110,13 +114,20 @@ def lesson(
)
# Create a formset that holds all personal notes for all persons in this lesson
persons_qs = lesson_period.get_personal_notes(wanted_week)
if not request.user.has_perm("alsijil.view_lesson_personalnote", lesson_period):
persons = Person.objects.filter(pk=request.user.person.pk)
else:
persons = Person.objects.all()
persons_qs = lesson_period.get_personal_notes(persons, wanted_week)
personal_note_formset = PersonalNoteFormSet(
request.POST or None, queryset=persons_qs, prefix="personal_notes"
)
if request.method == "POST":
if lesson_documentation_form.is_valid():
if lesson_documentation_form.is_valid() and request.user.has_perm(
"alsijil.edit_lessondocumentation", lesson_period
):
lesson_documentation_form.save()
messages.success(request, _("The lesson documentation has been saved."))
......@@ -126,7 +137,9 @@ def lesson(
not getattr(substitution, "cancelled", False)
or not get_site_preferences()["alsijil__block_personal_notes_for_cancelled"]
):
if personal_note_formset.is_valid():
if personal_note_formset.is_valid() and request.user.has_perm(
"alsijil.edit_lesson_personalnote", lesson_period
):
with reversion.create_revision():
instances = personal_note_formset.save()
......@@ -150,10 +163,13 @@ def lesson(
context["lesson_documentation"] = lesson_documentation
context["lesson_documentation_form"] = lesson_documentation_form
context["personal_note_formset"] = personal_note_formset
context["prev_lesson"] = lesson_period.prev
context["next_lesson"] = lesson_period.next
return render(request, "alsijil/class_register/lesson.html", context)
@permission_required("alsijil.view_week", fn=get_timetable_instance_by_pk)
def week_view(
request: HttpRequest,
year: Optional[int] = None,
......@@ -168,38 +184,30 @@ def week_view(
else:
wanted_week = CalendarWeek()
lesson_periods = LessonPeriod.objects.annotate(
has_documentation=Exists(
LessonDocumentation.objects.filter(
~Q(topic__exact=""),
lesson_period=OuterRef("pk"),
week=wanted_week.week,
year=wanted_week.year,
)
)
).in_week(wanted_week)
instance = get_timetable_instance_by_pk(request, year, week, type_, id_)
group = None
if type_ and id_:
instance = get_el_by_pk(request, type_, id_)
lesson_periods = LessonPeriod.objects.in_week(wanted_week).prefetch_related(
"lesson__groups__members",
"lesson__groups__parent_groups",
"lesson__groups__parent_groups__owners",
)
lesson_periods_query_exists = True
if type_ and id_:
if isinstance(instance, HttpResponseNotFound):
return HttpResponseNotFound()
type_ = TimetableType.from_string(type_)
if type_ == TimetableType.GROUP:
group = instance
lesson_periods = lesson_periods.filter_from_type(type_, instance)
elif hasattr(request, "user") and hasattr(request.user, "person"):
instance = request.user.person
if request.user.person.lessons_as_teacher.exists():
lesson_periods = lesson_periods.filter_teacher(request.user.person)
type_ = TimetableType.TEACHER
else:
lesson_periods = lesson_periods.filter_participant(request.user.person)
else:
lesson_periods_query_exists = False
lesson_periods = None
# Add a form to filter the view
......@@ -222,13 +230,48 @@ def week_view(
select_form.cleaned_data["instance"].pk,
)
if lesson_periods:
# Aggregate all personal notes for this group and week
if type_ == TimetableType.GROUP:
group = instance
else:
group = None
extra_marks = ExtraMark.objects.all()
if lesson_periods_query_exists:
lesson_periods_pk = list(lesson_periods.values_list("pk", flat=True))
lesson_periods = (
LessonPeriod.objects.prefetch_related(
Prefetch(
"documentations",
queryset=LessonDocumentation.objects.filter(
week=wanted_week.week, year=wanted_week.year
),
)
)
.filter(pk__in=lesson_periods_pk)
.annotate_week(wanted_week)
.annotate(
has_documentation=Exists(
LessonDocumentation.objects.filter(
~Q(topic__exact=""),
lesson_period=OuterRef("pk"),
week=wanted_week.week,
year=wanted_week.year,
)
)
)
.order_by("period__weekday", "period__period")
)
else:
lesson_periods_pk = []
if lesson_periods_pk:
# Aggregate all personal notes for this group and week
persons_qs = Person.objects.filter(is_active=True)
if group:
if not request.user.has_perm("alsijil.view_week_personalnote", instance):
persons_qs = persons_qs.filter(pk=request.user.person.pk)
elif group:
persons_qs = persons_qs.filter(member_of=group)
else:
persons_qs = persons_qs.filter(
......@@ -237,7 +280,17 @@ def week_view(
persons_qs = (
persons_qs.distinct()
.prefetch_related("personal_notes")
.prefetch_related(
Prefetch(
"personal_notes",
queryset=PersonalNote.objects.filter(
week=wanted_week.week,
year=wanted_week.year,
lesson_period__in=lesson_periods_pk,
),
),
"member_of__owners",
)
.annotate(
absences_count=Count(
"personal_notes",
......@@ -271,10 +324,20 @@ def week_view(
.annotate(tardiness_sum=Sum("personal_notes__late"))
.values("tardiness_sum")
),
tardiness_count=Count(
"personal_notes",
filter=Q(
personal_notes__lesson_period__in=lesson_periods_pk,
personal_notes__week=wanted_week.week,
personal_notes__year=wanted_week.year,
)
& ~Q(personal_notes__late=0),
distinct=True,
),
)
)
for extra_mark in ExtraMark.objects.all():
for extra_mark in extra_marks:
persons_qs = persons_qs.annotate(
**{
extra_mark.count_label: Count(
......@@ -293,24 +356,12 @@ def week_view(
persons = []
for person in persons_qs:
persons.append(
{
"person": person,
"personal_notes": person.personal_notes.filter(
week=wanted_week.week,
year=wanted_week.year,
lesson_period__in=lesson_periods_pk,
),
}
{"person": person, "personal_notes": list(person.personal_notes.all())}
)
else:
persons = None
# Resort lesson periods manually because an union queryset doesn't support order_by
lesson_periods = sorted(
lesson_periods, key=lambda x: (x.period.weekday, x.period.period)
)
context["extra_marks"] = ExtraMark.objects.all()
context["extra_marks"] = extra_marks
context["week"] = wanted_week
context["weeks"] = get_weeks_for_year(year=wanted_week.year)
context["lesson_periods"] = lesson_periods
......@@ -340,26 +391,30 @@ def week_view(
return render(request, "alsijil/class_register/week_view.html", context)
@permission_required(
"alsijil.view_full_register", fn=objectgetter_optional(Group, None, False)
)
def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
context = {}
group = get_object_or_404(Group, pk=id_)
current_school_term = SchoolTerm.current
if not current_school_term:
return HttpResponseNotFound(_("There is no current school term."))
# Get all lesson periods for the selected group
lesson_periods = (
LessonPeriod.objects.filter_group(group)
.filter(lesson__validity__school_term=current_school_term)
.distinct()
.prefetch_related("documentations", "personal_notes")
.prefetch_related(
"documentations",
"personal_notes",
"personal_notes__excuse_type",
"personal_notes__extra_marks",
"personal_notes__person",
"personal_notes__groups_of_person",
)
)
weeks = CalendarWeek.weeks_within(
current_school_term.date_start, current_school_term.date_end,
group.school_term.date_start, group.school_term.date_end,
)
periods_by_day = {}
......@@ -390,65 +445,20 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
(lesson_period, documentations, notes, substitution)
)
persons = Person.objects.filter(
personal_notes__groups_of_person=group,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
).annotate(
absences_count=Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
),
excused=Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__excused=True,
personal_notes__excuse_type__isnull=True,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
),
unexcused=Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__excused=False,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
),
tardiness=Sum("personal_notes__late"),
persons = Person.objects.prefetch_related(
"personal_notes",
"personal_notes__excuse_type",
"personal_notes__extra_marks",
"personal_notes__lesson_period__lesson__subject",
"personal_notes__lesson_period__substitutions",
"personal_notes__lesson_period__substitutions__subject",
"personal_notes__lesson_period__substitutions__teachers",
"personal_notes__lesson_period__lesson__teachers",
"personal_notes__lesson_period__period",
)
persons = group.generate_person_list_with_class_register_statistics(persons)
for extra_mark in ExtraMark.objects.all():
persons = persons.annotate(
**{
extra_mark.count_label: Count(
"personal_notes",
filter=Q(
personal_notes__extra_marks=extra_mark,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
)
}
)
for excuse_type in ExcuseType.objects.all():
persons = persons.annotate(
**{
excuse_type.count_label: Count(
"personal_notes__absent",
filter=Q(
personal_notes__absent=True,
personal_notes__excuse_type=excuse_type,
personal_notes__lesson_period__lesson__validity__school_term=current_school_term,
),
)
}
)
context["school_term"] = current_school_term
context["school_term"] = group.school_term
context["persons"] = persons
context["excuse_types"] = ExcuseType.objects.all()
context["extra_marks"] = ExtraMark.objects.all()
......@@ -457,33 +467,73 @@ def full_register_group(request: HttpRequest, id_: int) -> HttpResponse:
context["periods_by_day"] = periods_by_day
context["lesson_periods"] = lesson_periods
context["today"] = date.today()
context["lessons"] = (
group.lessons.all()
.select_related("validity", "subject")
.prefetch_related("teachers", "lesson_periods")
)
context["child_groups"] = group.child_groups.all().prefetch_related(
"lessons",
"lessons__validity",
"lessons__subject",
"lessons__teachers",
"lessons__lesson_periods",
)
return render(request, "alsijil/print/full_register.html", context)
@permission_required("alsijil.view_my_students")
def my_students(request: HttpRequest) -> HttpResponse:
context = {}
relevant_groups = (
Group.objects.for_current_school_term_or_all()
.annotate(lessons_count=Count("lessons"))
.filter(lessons_count__gt=0, owners=request.user.person)
request.user.person.get_owner_groups_with_lessons()
.annotate(has_parents=Exists(Group.objects.filter(child_groups=OuterRef("pk"))))
.filter(members__isnull=False)
.order_by("has_parents", "name")
.prefetch_related("members")
.distinct()
)
persons = Person.objects.filter(member_of__in=relevant_groups)
context["persons"] = persons
new_groups = []
for group in relevant_groups:
persons = group.generate_person_list_with_class_register_statistics()
new_groups.append((group, persons))
context["groups"] = new_groups
context["excuse_types"] = ExcuseType.objects.all()
context["extra_marks"] = ExtraMark.objects.all()
return render(request, "alsijil/class_register/persons.html", context)
@permission_required("alsijil.view_my_groups",)
def my_groups(request: HttpRequest) -> HttpResponse:
context = {}
groups = (
Group.objects.for_current_school_term_or_all()
.annotate(lessons_count=Count("lessons"))
.filter(lessons_count__gt=0, owners=request.user.person)
context["groups"] = request.user.person.get_owner_groups_with_lessons().annotate(
students_count=Count("members")
)
context["groups"] = groups
return render(request, "alsijil/class_register/groups.html", context)
class StudentsList(PermissionRequiredMixin, DetailView):
model = Group
template_name = "alsijil/class_register/students_list.html"
permission_required = "alsijil.view_students_list"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["group"] = self.object
context[
"persons"
] = self.object.generate_person_list_with_class_register_statistics()
context["extra_marks"] = ExtraMark.objects.all()
context["excuse_types"] = ExcuseType.objects.all()
return context
@permission_required(
"alsijil.view_person_overview",
fn=objectgetter_optional(Person, "request.user.person", True),
)
def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
context = {}
person = objectgetter_optional(
......@@ -514,6 +564,11 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
request.POST["date"], "%Y-%m-%d"
).date()
if not request.user.has_perm(
"alsijil.edit_person_overview_personalnote", person
):
raise PermissionDenied()
notes = person.personal_notes.filter(
week=date.isocalendar()[1],
lesson_period__period__weekday=date.weekday(),
......@@ -539,6 +594,8 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
note = PersonalNote.objects.get(
pk=int(request.POST["personal_note"])
)
if not request.user.has_perm("alsijil.edit_personalnote", note):
raise PermissionDenied()
if note.absent:
note.excused = True
note.excuse_type = excuse_type
......@@ -552,10 +609,23 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
person.refresh_from_db()
unexcused_absences = person.personal_notes.filter(absent=True, excused=False)
person_personal_notes = person.personal_notes.all().prefetch_related(
"lesson_period__lesson__groups",
"lesson_period__lesson__teachers",
"lesson_period__substitutions",
)
if request.user.has_perm("alsijil.view_person_overview_personalnote", person):
allowed_personal_notes = person_personal_notes.all()
else:
allowed_personal_notes = person_personal_notes.filter(
lesson_period__lesson__groups__owners=request.user.person
)
unexcused_absences = allowed_personal_notes.filter(absent=True, excused=False)
context["unexcused_absences"] = unexcused_absences
personal_notes = person.personal_notes.filter(
personal_notes = allowed_personal_notes.filter(
Q(absent=True) | Q(late__gt=0) | ~Q(remarks="") | Q(extra_marks__isnull=False)
).order_by(
"-lesson_period__lesson__validity__date_start",
......@@ -566,101 +636,115 @@ def overview_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
context["personal_notes"] = personal_notes
context["excuse_types"] = ExcuseType.objects.all()
school_terms = SchoolTerm.objects.all().order_by("-date_start")
stats = []
for school_term in school_terms:
stat = {}
personal_notes = PersonalNote.objects.filter(
person=person, lesson_period__lesson__validity__school_term=school_term
)
extra_marks = ExtraMark.objects.all()
excuse_types = ExcuseType.objects.all()
if request.user.has_perm("alsijil.view_person_statistics_personalnote", person):
school_terms = SchoolTerm.objects.all().order_by("-date_start")
stats = []
for school_term in school_terms:
stat = {}
personal_notes = PersonalNote.objects.filter(
person=person, lesson_period__lesson__validity__school_term=school_term
)
if not personal_notes.exists():
continue
if not personal_notes.exists():
continue
stat.update(
personal_notes.filter(absent=True).aggregate(absences_count=Count("absent"))
)
stat.update(
personal_notes.filter(
absent=True, excused=True, excuse_type__isnull=True
).aggregate(excused=Count("absent"))
)
stat.update(
personal_notes.filter(absent=True, excused=False).aggregate(
unexcused=Count("absent")
stat.update(
personal_notes.filter(absent=True).aggregate(
absences_count=Count("absent")
)
)
stat.update(
personal_notes.filter(
absent=True, excused=True, excuse_type__isnull=True
).aggregate(excused=Count("absent"))
)
)
stat.update(personal_notes.aggregate(tardiness=Sum("late")))
for extra_mark in ExtraMark.objects.all():
stat.update(
personal_notes.filter(extra_marks=extra_mark).aggregate(
**{extra_mark.count_label: Count("pk")}
personal_notes.filter(absent=True, excused=False).aggregate(
unexcused=Count("absent")
)
)
for excuse_type in ExcuseType.objects.all():
stat.update(personal_notes.aggregate(tardiness=Sum("late")))
stat.update(
personal_notes.filter(absent=True, excuse_type=excuse_type).aggregate(
**{excuse_type.count_label: Count("absent")}
personal_notes.filter(~Q(late=0)).aggregate(
tardiness_count=Count("late")
)
)
stats.append((school_term, stat))
context["stats"] = stats
context["excuse_types"] = ExcuseType.objects.all()
context["extra_marks"] = ExtraMark.objects.all()
for extra_mark in extra_marks:
stat.update(
personal_notes.filter(extra_marks=extra_mark).aggregate(
**{extra_mark.count_label: Count("pk")}
)
)
for excuse_type in excuse_types:
stat.update(
personal_notes.filter(
absent=True, excuse_type=excuse_type
).aggregate(**{excuse_type.count_label: Count("absent")})
)
stats.append((school_term, stat))
context["stats"] = stats
context["excuse_types"] = excuse_types
context["extra_marks"] = extra_marks
return render(request, "alsijil/class_register/person.html", context)
def register_absence(request: HttpRequest) -> HttpResponse:
@permission_required("alsijil.register_absence", fn=objectgetter_optional(Person))
def register_absence(request: HttpRequest, id_: int) -> HttpResponse:
context = {}
person = get_object_or_404(Person, pk=id_)
register_absence_form = RegisterAbsenceForm(request.POST or None)
if request.method == "POST":
if register_absence_form.is_valid():
# Get data from form
person = register_absence_form.cleaned_data["person"]
start_date = register_absence_form.cleaned_data["date_start"]
end_date = register_absence_form.cleaned_data["date_end"]
from_period = register_absence_form.cleaned_data["from_period"]
to_period = register_absence_form.cleaned_data["to_period"]
absent = register_absence_form.cleaned_data["absent"]
excused = register_absence_form.cleaned_data["excused"]
excuse_type = register_absence_form.cleaned_data["excuse_type"]
remarks = register_absence_form.cleaned_data["remarks"]
# Mark person as absent
delta = end_date - start_date
for i in range(delta.days + 1):
from_period_on_day = from_period if i == 0 else TimePeriod.period_min
to_period_on_day = (
to_period if i == delta.days else TimePeriod.period_max
)
day = start_date + timedelta(days=i)
person.mark_absent(
day,
from_period_on_day,
absent,
excused,
excuse_type,
remarks,
to_period_on_day,
)
if request.method == "POST" and register_absence_form.is_valid():
# Get data from form
# person = register_absence_form.cleaned_data["person"]
start_date = register_absence_form.cleaned_data["date_start"]
end_date = register_absence_form.cleaned_data["date_end"]
from_period = register_absence_form.cleaned_data["from_period"]
to_period = register_absence_form.cleaned_data["to_period"]
absent = register_absence_form.cleaned_data["absent"]
excused = register_absence_form.cleaned_data["excused"]
excuse_type = register_absence_form.cleaned_data["excuse_type"]
remarks = register_absence_form.cleaned_data["remarks"]
# Mark person as absent
delta = end_date - start_date
for i in range(delta.days + 1):
from_period_on_day = from_period if i == 0 else TimePeriod.period_min
to_period_on_day = to_period if i == delta.days else TimePeriod.period_max
day = start_date + timedelta(days=i)
person.mark_absent(
day,
from_period_on_day,
absent,
excused,
excuse_type,
remarks,
to_period_on_day,
)
messages.success(request, _("The absence has been saved."))
return redirect("register_absence")
messages.success(request, _("The absence has been saved."))
return redirect("overview_person", person.pk)
context["person"] = person
context["register_absence_form"] = register_absence_form
return render(request, "alsijil/absences/register.html", context)
class DeletePersonalNoteView(DetailView):
class DeletePersonalNoteView(PermissionRequiredMixin, DetailView):
model = PersonalNote
template_name = "core/pages/delete.html"
permission_required = "alsijil.edit_personalnote"
def post(self, request, *args, **kwargs):
note = self.get_object()
......@@ -671,83 +755,83 @@ class DeletePersonalNoteView(DetailView):
return redirect("overview_person", note.person.pk)
class ExtraMarkListView(SingleTableView, PermissionRequiredMixin):
class ExtraMarkListView(PermissionRequiredMixin, SingleTableView):
"""Table of all extra marks."""
model = ExtraMark
table_class = ExtraMarkTable
permission_required = "core.view_extramark"
permission_required = "alsijil.view_extramark"
template_name = "alsijil/extra_mark/list.html"
class ExtraMarkCreateView(AdvancedCreateView, PermissionRequiredMixin):
class ExtraMarkCreateView(PermissionRequiredMixin, AdvancedCreateView):
"""Create view for extra marks."""
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "core.create_extramark"
permission_required = "alsijil.create_extramark"
template_name = "alsijil/extra_mark/create.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been created.")
class ExtraMarkEditView(AdvancedEditView, PermissionRequiredMixin):
class ExtraMarkEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for extra marks."""
model = ExtraMark
form_class = ExtraMarkForm
permission_required = "core.edit_extramark"
permission_required = "alsijil.edit_extramark"
template_name = "alsijil/extra_mark/edit.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been saved.")
class ExtraMarkDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
class ExtraMarkDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""Delete view for extra marks"""
model = ExtraMark
permission_required = "core.delete_extramark"
permission_required = "alsijil.delete_extramark"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("extra_marks")
success_message = _("The extra mark has been deleted.")
class ExcuseTypeListView(SingleTableView, PermissionRequiredMixin):
class ExcuseTypeListView(PermissionRequiredMixin, SingleTableView):
"""Table of all excuse types."""
model = ExcuseType
table_class = ExcuseTypeTable
permission_required = "core.view_excusetype"
permission_required = "alsijil.view_excusetypes"
template_name = "alsijil/excuse_type/list.html"
class ExcuseTypeCreateView(AdvancedCreateView, PermissionRequiredMixin):
class ExcuseTypeCreateView(PermissionRequiredMixin, AdvancedCreateView):
"""Create view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "core.create_excusetype"
permission_required = "alsijil.add_excusetype"
template_name = "alsijil/excuse_type/create.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been created.")
class ExcuseTypeEditView(AdvancedEditView, PermissionRequiredMixin):
class ExcuseTypeEditView(PermissionRequiredMixin, AdvancedEditView):
"""Edit view for excuse types."""
model = ExcuseType
form_class = ExcuseTypeForm
permission_required = "core.edit_excusetype"
permission_required = "alsijil.edit_excusetype"
template_name = "alsijil/excuse_type/edit.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been saved.")
class ExcuseTypeDeleteView(AdvancedDeleteView, PermissionRequiredMixin, RevisionMixin):
class ExcuseTypeDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
"""Delete view for excuse types"""
model = ExcuseType
permission_required = "core.delete_excusetype"
permission_required = "alsijil.delete_excusetype"
template_name = "core/pages/delete.html"
success_url = reverse_lazy("excuse_types")
success_message = _("The excuse type has been deleted.")
......@@ -68,11 +68,11 @@ extras = ["phonenumbers"]
version = ">=3.0,<4.0"
[package.dependencies.django-two-factor-auth]
extras = ["yubikey", "call", "phonenumbers", "sms"]
extras = ["yubikey", "phonenumbers", "sms", "call"]
version = ">=1.11.0,<2.0.0"
[package.dependencies.dynaconf]
extras = ["ini", "yaml", "toml"]
extras = ["ini", "toml", "yaml"]
version = ">=2.0,<3.0"
[package.extras]
......@@ -1035,7 +1035,7 @@ description = "Python docstring reStructuredText (RST) validator"
name = "flake8-rst-docstrings"
optional = false
python-versions = "*"
version = "0.0.13"
version = "0.0.14"
[package.dependencies]
flake8 = ">=3.0.0"
......@@ -1949,7 +1949,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "4345c57e5e3244ce0bbdc8ad8a7cffa385fe33b7da1b5fc87ffe32dabf45c5a9"
content-hash = "f647d48cd770fea71d41ea5c67cda07371a03c17d080f02f4ac4dc1f840b2b40"
python-versions = "^3.7"
[metadata.files]
......@@ -2295,7 +2295,7 @@ flake8-polyfill = [
{file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
]
flake8-rst-docstrings = [
{file = "flake8-rst-docstrings-0.0.13.tar.gz", hash = "sha256:b1b619d81d879b874533973ac04ee5d823fdbe8c9f3701bfe802bb41813997b4"},
{file = "flake8-rst-docstrings-0.0.14.tar.gz", hash = "sha256:8f8bcb18f1408b506dd8ba2c99af3eac6128f6911d4bf6ff874b94caa70182a2"},
]
gitdb = [
{file = "gitdb-4.0.5-py3-none-any.whl", hash = "sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac"},
......@@ -2463,6 +2463,8 @@ pillow = [
{file = "Pillow-7.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:5e51ee2b8114def244384eda1c82b10e307ad9778dac5c83fb0943775a653cd8"},
{file = "Pillow-7.2.0-cp38-cp38-win32.whl", hash = "sha256:725aa6cfc66ce2857d585f06e9519a1cc0ef6d13f186ff3447ab6dff0a09bc7f"},
{file = "Pillow-7.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:a060cf8aa332052df2158e5a119303965be92c3da6f2d93b6878f0ebca80b2f6"},
{file = "Pillow-7.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:9c87ef410a58dd54b92424ffd7e28fd2ec65d2f7fc02b76f5e9b2067e355ebf6"},
{file = "Pillow-7.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e901964262a56d9ea3c2693df68bc9860b8bdda2b04768821e4c44ae797de117"},
{file = "Pillow-7.2.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:25930fadde8019f374400f7986e8404c8b781ce519da27792cbe46eabec00c4d"},
{file = "Pillow-7.2.0.tar.gz", hash = "sha256:97f9e7953a77d5a70f49b9a48da7776dc51e9b738151b22dacf101641594a626"},
]
......@@ -2504,25 +2506,30 @@ pycryptodome = [
{file = "pycryptodome-3.9.8-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2"},
{file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2"},
{file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345"},
{file = "pycryptodome-3.9.8-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f"},
{file = "pycryptodome-3.9.8-cp35-cp35m-win32.whl", hash = "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23"},
{file = "pycryptodome-3.9.8-cp35-cp35m-win_amd64.whl", hash = "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b"},
{file = "pycryptodome-3.9.8-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299"},
{file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982"},
{file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1"},
{file = "pycryptodome-3.9.8-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e"},
{file = "pycryptodome-3.9.8-cp36-cp36m-win32.whl", hash = "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739"},
{file = "pycryptodome-3.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e"},
{file = "pycryptodome-3.9.8-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a"},
{file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c"},
{file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21"},
{file = "pycryptodome-3.9.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc"},
{file = "pycryptodome-3.9.8-cp37-cp37m-win32.whl", hash = "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876"},
{file = "pycryptodome-3.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8"},
{file = "pycryptodome-3.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a"},
{file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149"},
{file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6"},
{file = "pycryptodome-3.9.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7"},
{file = "pycryptodome-3.9.8-cp38-cp38-win32.whl", hash = "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba"},
{file = "pycryptodome-3.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68"},
{file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_i686.whl", hash = "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60"},
{file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a"},
{file = "pycryptodome-3.9.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd"},
{file = "pycryptodome-3.9.8.tar.gz", hash = "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4"},
]
pydocstyle = [
......
......@@ -41,7 +41,7 @@ flake8-mypy = "^17.8.0"
flake8-bandit = "^2.1.2"
flake8-builtins = "^1.4.1"
flake8-docstrings = "^1.5.0"
flake8-rst-docstrings = "^0.0.13"
flake8-rst-docstrings = "^0.0.14"
black = "^19.10b0"
flake8-black = "^0.2.0"
isort = "^5.0.0"
......