diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 7abfa8ca..9ba72e7d 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -1031,7 +1031,7 @@ thead { } tbody > tr { - &:nth-child(even) { + &:nth-child(even):not(.highlight) { background: $primary-neutral-light-color; } &.clickable:hover { diff --git a/core/tests.py b/core/tests.py index a2e9c526..61d497a5 100644 --- a/core/tests.py +++ b/core/tests.py @@ -15,17 +15,18 @@ # import os -from datetime import timedelta +from datetime import date, timedelta +import freezegun from django.core.cache import cache from django.test import Client, TestCase from django.urls import reverse -from django.core.management import call_command from django.utils.timezone import now from club.models import Membership -from core.models import User, Group, Page, AnonymousUser from core.markdown import markdown +from core.models import AnonymousUser, Group, Page, User +from core.utils import get_semester_code, get_start_of_semester from sith import settings """ @@ -617,3 +618,75 @@ class UserIsInGroupTest(TestCase): returns False """ self.assertFalse(self.skia.is_in_group(name="This doesn't exist")) + + +class DateUtilsTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.autumn_month = settings.SITH_SEMESTER_START_AUTUMN[0] + cls.autumn_day = settings.SITH_SEMESTER_START_AUTUMN[1] + cls.spring_month = settings.SITH_SEMESTER_START_SPRING[0] + cls.spring_day = settings.SITH_SEMESTER_START_SPRING[1] + + cls.autumn_semester_january = date(2025, 1, 4) + cls.autumn_semester_september = date(2024, 9, 4) + cls.autumn_first_day = date(2024, cls.autumn_month, cls.autumn_day) + + cls.spring_semester_march = date(2023, 3, 4) + cls.spring_first_day = date(2023, cls.spring_month, cls.spring_day) + + def test_get_semester(self): + """ + Test that the get_semester function returns the correct semester string + """ + self.assertEqual(get_semester_code(self.autumn_semester_january), "A24") + self.assertEqual(get_semester_code(self.autumn_semester_september), "A24") + self.assertEqual(get_semester_code(self.autumn_first_day), "A24") + + self.assertEqual(get_semester_code(self.spring_semester_march), "P23") + self.assertEqual(get_semester_code(self.spring_first_day), "P23") + + def test_get_start_of_semester_fixed_date(self): + """ + Test that the get_start_of_semester correctly the starting date of the semester. + """ + automn_2024 = date(2024, self.autumn_month, self.autumn_day) + self.assertEqual( + get_start_of_semester(self.autumn_semester_january), automn_2024 + ) + self.assertEqual( + get_start_of_semester(self.autumn_semester_september), automn_2024 + ) + self.assertEqual(get_start_of_semester(self.autumn_first_day), automn_2024) + + spring_2023 = date(2023, self.spring_month, self.spring_day) + self.assertEqual(get_start_of_semester(self.spring_semester_march), spring_2023) + self.assertEqual(get_start_of_semester(self.spring_first_day), spring_2023) + + def test_get_start_of_semester_today(self): + """ + Test that the get_start_of_semester returns the start of the current semester + when no date is given + """ + with freezegun.freeze_time(self.autumn_semester_september): + self.assertEqual(get_start_of_semester(), self.autumn_first_day) + + with freezegun.freeze_time(self.spring_semester_march): + self.assertEqual(get_start_of_semester(), self.spring_first_day) + + def test_get_start_of_semester_changing_date(self): + """ + Test that the get_start_of_semester correctly gives the starting date of the semester, + even when the semester changes while the server isn't restarted. + """ + spring_2023 = date(2023, self.spring_month, self.spring_day) + autumn_2023 = date(2023, self.autumn_month, self.autumn_day) + mid_spring = spring_2023 + timedelta(days=45) + mid_autumn = autumn_2023 + timedelta(days=45) + + with freezegun.freeze_time(mid_spring) as frozen_time: + self.assertEqual(get_start_of_semester(), spring_2023) + + # forward time to the middle of the next semester + frozen_time.move_to(mid_autumn) + self.assertEqual(get_start_of_semester(), autumn_2023) diff --git a/core/utils.py b/core/utils.py index a053e2d5..d30e3ebf 100644 --- a/core/utils.py +++ b/core/utils.py @@ -15,20 +15,19 @@ # import os -import subprocess import re - -# Image utils - -from io import BytesIO +import subprocess from datetime import date -from PIL import ExifTags +# Image utils +from io import BytesIO +from typing import Optional import PIL - from django.conf import settings from django.core.files.base import ContentFile +from PIL import ExifTags +from django.utils import timezone def get_git_revision_short_hash() -> str: @@ -44,34 +43,54 @@ def get_git_revision_short_hash() -> str: return "" -def get_start_of_semester(d=date.today()): +def get_start_of_semester(today: Optional[date] = None) -> date: """ - This function computes the start date of the semester with respect to the given date (default is today), - and the start date given in settings.SITH_START_DATE. - It takes the nearest past start date. - Exemples: with SITH_START_DATE = (8, 15) - Today -> Start date - 2015-03-17 -> 2015-02-15 - 2015-01-11 -> 2014-08-15 + Return the date of the start of the semester of the given date. + If no date is given, return the start date of the current semester. + + The current semester is computed as follows: + + - If the date is between 15/08 and 31/12 => Autumn semester. + - If the date is between 01/01 and 15/02 => Autumn semester of the previous year. + - If the date is between 15/02 and 15/08 => Spring semester + + :param today: the date to use to compute the semester. If None, use today's date. + :return: the date of the start of the semester """ - today = d - year = today.year - start = date(year, settings.SITH_START_DATE[0], settings.SITH_START_DATE[1]) - start2 = start.replace(month=(start.month + 6) % 12) - spring, autumn = min(start, start2), max(start, start2) - if today > autumn: # autumn semester + if today is None: + today = timezone.now().date() + + autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN) + spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING) + + if today >= autumn: # between 15/08 (included) and 31/12 -> autumn semester return autumn - if today > spring: # spring semester + if today >= spring: # between 15/02 (included) and 15/08 -> spring semester return spring - return autumn.replace(year=year - 1) # autumn semester of last year + # between 01/01 and 15/02 -> autumn semester of the previous year + return autumn.replace(year=autumn.year - 1) -def get_semester(d=date.today()): +def get_semester_code(d: Optional[date] = None) -> str: + """ + Return the semester code of the given date. + If no date is given, return the semester code of the current semester. + + The semester code is an upper letter (A for autumn, P for spring), + followed by the last two digits of the year. + For example, the autumn semester of 2018 is "A18". + + :param d: the date to use to compute the semester. If None, use today's date. + :return: the semester code corresponding to the given date + """ + if d is None: + d = timezone.now().date() + start = get_start_of_semester(d) - if start.month <= 6: - return "P" + str(start.year)[-2:] - else: + + if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN: return "A" + str(start.year)[-2:] + return "P" + str(start.year)[-2:] def file_exist(path): diff --git a/counter/models.py b/counter/models.py index c6389349..476aaf13 100644 --- a/counter/models.py +++ b/counter/models.py @@ -15,7 +15,7 @@ # from __future__ import annotations -from typing import Tuple +from typing import Tuple, Optional from django.db import models from django.db.models import F, Value, Sum, QuerySet, OuterRef, Exists @@ -536,7 +536,7 @@ class Counter(models.Model): .order_by("-perm_sum") ) - def get_top_customers(self, since=get_start_of_semester()) -> QuerySet: + def get_top_customers(self, since: Optional[date] = None) -> QuerySet: """ Return a QuerySet querying the money spent by customers of this counter since the specified date, ordered by descending amount of money spent. @@ -546,6 +546,8 @@ class Counter(models.Model): - the nickname of the customer - the amount of money spent by the customer """ + if since is None: + since = get_start_of_semester() return ( self.sellings.filter(date__gte=since) .annotate( @@ -557,7 +559,8 @@ class Counter(models.Model): ) .annotate(nickname=F("customer__user__nick_name")) .annotate(promo=F("customer__user__promo")) - .values("customer__user", "name", "nickname") + .annotate(user=F("customer__user")) + .values("user", "promo", "name", "nickname") .annotate( selling_sum=Sum( F("unit_price") * F("quantity"), output_field=CurrencyField() @@ -567,15 +570,17 @@ class Counter(models.Model): .order_by("-selling_sum") ) - def get_total_sales(self, since=get_start_of_semester()) -> CurrencyField: + def get_total_sales(self, since=None) -> CurrencyField: """ Compute and return the total turnover of this counter since the date specified in parameter (by default, since the start of the current semester) :param since: timestamp from which to perform the calculation - :type since: datetime | date + :type since: datetime | date | None :return: Total revenue earned at this counter """ + if since is None: + since = get_start_of_semester() if isinstance(since, date): since = datetime.combine(since, datetime.min.time()) total = self.sellings.filter(date__gte=since).aggregate( diff --git a/counter/templates/counter/stats.jinja b/counter/templates/counter/stats.jinja index 03b7f4e0..d6cc14f0 100644 --- a/counter/templates/counter/stats.jinja +++ b/counter/templates/counter/stats.jinja @@ -11,7 +11,9 @@ {% block content %}