diff --git a/aleksis/apps/chronos/admin.py b/aleksis/apps/chronos/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..b43af26cb5d602b1e5584e2be32f6adf2f2294fe --- /dev/null +++ b/aleksis/apps/chronos/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import TimetableWidget + +admin.site.register(TimetableWidget) diff --git a/aleksis/apps/chronos/migrations/0001_initial.py b/aleksis/apps/chronos/migrations/0001_initial.py index 28eab760977e1f3290581cf1930a09a1c2ad7fc5..a5273c7889a7d9d4fa9eaeadb0e144f9e9ff1a28 100644 --- a/aleksis/apps/chronos/migrations/0001_initial.py +++ b/aleksis/apps/chronos/migrations/0001_initial.py @@ -4,7 +4,7 @@ import django.core.validators import django.db.models.deletion from django.db import migrations, models -import aleksis.apps.chronos.util.weeks +import aleksis.apps.chronos.util.date import aleksis.core.util.core_helpers @@ -229,7 +229,7 @@ class Migration(migrations.Migration): ( "week", models.IntegerField( - default=aleksis.apps.chronos.util.weeks.CalendarWeek.current_week, + default=aleksis.apps.chronos.util.date.CalendarWeek.current_week, verbose_name="Week", ), ), diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py index 8f16b828ae522f64daa3df7371871c8afcc4a0c3..672600908bbb9d847ce8fae762ca5b1dd5da4b9b 100644 --- a/aleksis/apps/chronos/model_extensions.py +++ b/aleksis/apps/chronos/model_extensions.py @@ -1,4 +1,6 @@ -from aleksis.core.models import Person +from typing import Optional, Union + +from aleksis.core.models import Person, Group from .models import Lesson, LessonPeriod @@ -10,6 +12,32 @@ def is_teacher(self): return self.lesson_periods_as_teacher.exists() +@Person.property +def timetable_type(self) -> Optional[str]: + """ Return which type of timetable this user has """ + + if self.is_teacher: + return "teacher" + elif self.primary_group: + return "group" + else: + return None + + +@Person.property +def timetable_object(self) -> Optional[Union[Group, Person]]: + """ Return the object which has the user's timetable """ + + type_ = self.timetable_type + + if type_ == "teacher": + return self + elif type_ == "group": + return self.primary_group + else: + return None + + @Person.property def lessons_as_participant(self): """ Return a `QuerySet` containing all `Lesson`s this person diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index 8cfdb3fec8e2f10f9bd4636374804f86deb3622c..5a6eb38f9bf4b308e0029506700751322a1a249e 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import OrderedDict from datetime import date, datetime, timedelta, time from typing import Dict, Optional, Tuple, Union @@ -8,6 +9,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import F, Max, Min, Q from django.db.models.functions import Coalesce +from django.forms import Media from django.http.request import QueryDict from django.urls import reverse from django.utils import timezone @@ -15,11 +17,13 @@ from django.utils.decorators import classproperty from django.utils.translation import ugettext_lazy as _ from calendarweek.django import CalendarWeek, i18n_day_names_lazy, i18n_day_abbrs_lazy +from django_global_request.middleware import get_request from aleksis.core.mixins import ExtensibleModel -from aleksis.core.models import Group, Person +from aleksis.core.models import Group, Person, DashboardWidget -from aleksis.apps.chronos.util.weeks import week_weekday_from_date +from aleksis.apps.chronos.util.date import week_weekday_from_date +from aleksis.core.util.core_helpers import has_person class LessonPeriodManager(models.Manager): @@ -202,6 +206,41 @@ class LessonPeriodQuerySet(LessonDataQuerySet): else: return None + def filter_from_person(self, person: Person) -> Optional[models.QuerySet]: + type_ = person.timetable_type + + if type_ == "teacher": + # Teacher + + return person.lesson_periods_as_teacher + + elif type_ == "group": + # Student + + return person.lesson_periods_as_participant + + else: + # If no student or teacher + return None + + def daily_lessons_for_person(self, person: Person, wanted_day: date) -> Optional[models.QuerySet]: + lesson_periods = LessonPeriod.objects.filter_from_person(person) + + if lesson_periods is None: + return None + + return lesson_periods.on_day(wanted_day) + + def per_period_one_day(self) -> OrderedDict: + """ Group selected lessons per period for one day """ + per_period = {} + for lesson_period in self: + if lesson_period.period.period in per_period: + per_period[lesson_period.period.period].append(lesson_period) + else: + per_period[lesson_period.period.period] = [lesson_period] + return OrderedDict(sorted(per_period.items())) + class LessonSubstitutionQuerySet(LessonDataQuerySet): _period_path = "lesson_period__" @@ -505,3 +544,40 @@ class LessonPeriod(ExtensibleModel): class Meta: ordering = ["lesson__date_start", "period__weekday", "period__period"] indexes = [models.Index(fields=["lesson", "period"])] + + +class TimetableWidget(DashboardWidget): + template = "chronos/widget.html" + + def get_context(self): + request = get_request() + context = {"has_plan": True} + wanted_day = TimePeriod.get_next_relevant_day(timezone.now().date(), datetime.now().time()) + + if has_person(request.user): + person = request.user.person + + lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day) + type_ = person.timetable_type + + if type_ is None: + # If no student or teacher, redirect to all timetables + context["has_plan"] = False + else: + context["lesson_periods"] = lesson_periods.per_period_one_day() + context["type"] = type_ + context["day"] = wanted_day + context["periods"] = TimePeriod.get_times_dict() + context["smart"] = True + else: + context["has_plan"] = False + + return context + + media = Media(css={ + "all": ("css/chronos/timetable.css",) + }) + + class Meta: + proxy = True + verbose_name = _("Timetable widget") diff --git a/aleksis/apps/chronos/templates/chronos/my_timetable.html b/aleksis/apps/chronos/templates/chronos/my_timetable.html index 90adf8fb4a2f0c04eeb605ae7ff6b271a6ab6141..d508fd43b0b02009c7124e5bb03c4bde2b77e8e5 100644 --- a/aleksis/apps/chronos/templates/chronos/my_timetable.html +++ b/aleksis/apps/chronos/templates/chronos/my_timetable.html @@ -50,18 +50,9 @@ </div> </div> + {# Lessons #} - {% for period, lessons in lesson_periods.items %} - <div class="row"> - <div class="col s4"> - {% include "chronos/partials/period_time.html" with period=period periods=periods %} - </div> - <div class="col s8"> - {# A lesson #} - {% include "chronos/partials/lesson.html" with lessons=lessons %} - </div> - </div> - {% endfor %} + {% include "chronos/partials/lessons_col.html" with lesson_periods=lesson_periods %} </div> </div> diff --git a/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html new file mode 100644 index 0000000000000000000000000000000000000000..abb2710a6a147d2bbc67ab17be784a6c52f5da69 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/partials/lessons_col.html @@ -0,0 +1,10 @@ +{% for period, lessons in lesson_periods.items %} + <div class="row"> + <div class="col s4"> + {% include "chronos/partials/period_time.html" with period=period periods=periods %} + </div> + <div class="col s8"> + {% include "chronos/partials/lesson.html" with lessons=lessons %} + </div> + </div> +{% endfor %} diff --git a/aleksis/apps/chronos/templates/chronos/widget.html b/aleksis/apps/chronos/templates/chronos/widget.html new file mode 100644 index 0000000000000000000000000000000000000000..7c4e112bd4c599d8e93df363588005467379bde3 --- /dev/null +++ b/aleksis/apps/chronos/templates/chronos/widget.html @@ -0,0 +1,32 @@ +{# -*- engine:django -*- #} + +{% load i18n static humanize %} + +<div class="card"> + <div class="card-content"> + <span class="card-title"> + {% blocktrans with day=day|naturalday:"l" %} + My timetable for {{ day }} + {% endblocktrans %} + </span> + <div class="timetable-plan"> + {% if has_plan %} + {% include "chronos/partials/lessons_col.html" with lesson_periods=lesson_periods %} + {% else %} + <div class="alert warning"> + <p> + <i class="material-icons left">info</i> + {% blocktrans %} + There is no timetable linked to your person. + {% endblocktrans %} + </p> + </div> + {% endif %} + </div> + </div> + {% if has_plan %} + <div class="card-action"> + <a href="{% url "my_timetable" %}">{% trans "Go to smart plan" %}</a> + </div> + {% endif %} +</div> diff --git a/aleksis/apps/chronos/templatetags/week_helpers.py b/aleksis/apps/chronos/templatetags/week_helpers.py index 26dee1e900e84367e8ec4b1efbcdd8c6d298499e..acfb3a3a8859cc50c9e250520d9bcb32f3764ebe 100644 --- a/aleksis/apps/chronos/templatetags/week_helpers.py +++ b/aleksis/apps/chronos/templatetags/week_helpers.py @@ -4,7 +4,7 @@ from typing import Optional, Union from django import template from django.db.models.query import QuerySet -from aleksis.apps.chronos.util.weeks import CalendarWeek, week_period_to_date, week_weekday_to_date +from aleksis.apps.chronos.util.date import CalendarWeek, week_period_to_date, week_weekday_to_date register = template.Library() diff --git a/aleksis/apps/chronos/util/weeks.py b/aleksis/apps/chronos/util/date.py similarity index 85% rename from aleksis/apps/chronos/util/weeks.py rename to aleksis/apps/chronos/util/date.py index 32ec8b81f006d0332b5be8b1502043a1c1806766..f17aa0ba8354b9dce1ed9db5ee158954afc71ea0 100644 --- a/aleksis/apps/chronos/util/weeks.py +++ b/aleksis/apps/chronos/util/date.py @@ -2,6 +2,9 @@ from datetime import date from typing import Tuple, List, Union from calendarweek import CalendarWeek +from calendarweek.django import i18n_day_names +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ def week_weekday_from_date(when: date) -> Tuple[CalendarWeek, int]: diff --git a/aleksis/apps/chronos/views.py b/aleksis/apps/chronos/views.py index d885886ee35ed22f9b0690fa627a9d100cf190cf..daad66d7c699cfd95d711ad9faa2760274fe8049 100644 --- a/aleksis/apps/chronos/views.py +++ b/aleksis/apps/chronos/views.py @@ -19,7 +19,7 @@ from .forms import LessonSubstitutionForm from .models import LessonPeriod, LessonSubstitution, TimePeriod, Room from .tables import LessonsTable from .util.js import date_unix -from .util.weeks import CalendarWeek, get_weeks_for_year +from .util.date import CalendarWeek, get_weeks_for_year from aleksis.core.util.core_helpers import has_person @@ -62,46 +62,29 @@ def my_timetable( if has_person(request.user): person = request.user.person - if person.is_teacher: - # Teacher + lesson_periods = LessonPeriod.objects.daily_lessons_for_person(person, wanted_day) - type_ = "teacher" - super_el = person - lesson_periods_person = person.lesson_periods_as_teacher - - elif person.primary_group: - # Student - - type_ = "group" - super_el = person.primary_group - lesson_periods_person = person.lesson_periods_as_participant - - else: + if lesson_periods is None: # If no student or teacher, redirect to all timetables return redirect("all_timetables") - lesson_periods = lesson_periods_person.on_day(wanted_day) - - # Build dictionary with lessons - per_period = {} - for lesson_period in lesson_periods: - if lesson_period.period.period in per_period: - per_period[lesson_period.period.period].append(lesson_period) - else: - per_period[lesson_period.period.period] = [lesson_period] + type_ = person.timetable_type + super_el = person.timetable_object - context["lesson_periods"] = OrderedDict(sorted(per_period.items())) - context["super"] = {"type": type_, "el": super_el} - context["type"] = type_ - context["day"] = wanted_day - context["periods"] = TimePeriod.get_times_dict() - context["smart"] = True + context["lesson_periods"] = lesson_periods.per_period_one_day() + context["super"] = {"type": type_, "el": super_el} + context["type"] = type_ + context["day"] = wanted_day + context["periods"] = TimePeriod.get_times_dict() + context["smart"] = True - context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day( - wanted_day, "my_timetable_by_date" - ) + context["url_prev"], context["url_next"] = TimePeriod.get_prev_next_by_day( + wanted_day, "my_timetable_by_date" + ) - return render(request, "chronos/my_timetable.html", context) + return render(request, "chronos/my_timetable.html", context) + else: + return redirect("all_timetables") @login_required