Fix immutable default variable in get_start_of_semester (#656)

Le serveur ne percevait pas le changement de semestre, parce
que la valeur par défaut passée à la fonction `get_start_of_semester()` était une fonction appelée une seule fois, lors du lancement du serveur. Bref, c'était ça : https://beta.ruff.rs/docs/rules/function-call-in-default-argument/

---------

Co-authored-by: imperosol <thgirod@hotmail.com>
This commit is contained in:
Julien Constant 2023-09-07 23:11:58 +02:00 committed by GitHub
parent 544b0248b2
commit 38295e591d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 194 additions and 68 deletions

View File

@ -1031,7 +1031,7 @@ thead {
} }
tbody > tr { tbody > tr {
&:nth-child(even) { &:nth-child(even):not(.highlight) {
background: $primary-neutral-light-color; background: $primary-neutral-light-color;
} }
&.clickable:hover { &.clickable:hover {

View File

@ -15,17 +15,18 @@
# #
import os import os
from datetime import timedelta from datetime import date, timedelta
import freezegun
from django.core.cache import cache from django.core.cache import cache
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.core.management import call_command
from django.utils.timezone import now from django.utils.timezone import now
from club.models import Membership from club.models import Membership
from core.models import User, Group, Page, AnonymousUser
from core.markdown import markdown 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 from sith import settings
""" """
@ -617,3 +618,75 @@ class UserIsInGroupTest(TestCase):
returns False returns False
""" """
self.assertFalse(self.skia.is_in_group(name="This doesn't exist")) 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)

View File

@ -15,20 +15,19 @@
# #
import os import os
import subprocess
import re import re
import subprocess
# Image utils
from io import BytesIO
from datetime import date from datetime import date
from PIL import ExifTags # Image utils
from io import BytesIO
from typing import Optional
import PIL import PIL
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from PIL import ExifTags
from django.utils import timezone
def get_git_revision_short_hash() -> str: def get_git_revision_short_hash() -> str:
@ -44,34 +43,54 @@ def get_git_revision_short_hash() -> str:
return "" 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), Return the date of the start of the semester of the given date.
and the start date given in settings.SITH_START_DATE. If no date is given, return the start date of the current semester.
It takes the nearest past start date.
Exemples: with SITH_START_DATE = (8, 15) The current semester is computed as follows:
Today -> Start date
2015-03-17 -> 2015-02-15 - If the date is between 15/08 and 31/12 => Autumn semester.
2015-01-11 -> 2014-08-15 - 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 if today is None:
year = today.year today = timezone.now().date()
start = date(year, settings.SITH_START_DATE[0], settings.SITH_START_DATE[1])
start2 = start.replace(month=(start.month + 6) % 12) autumn = date(today.year, *settings.SITH_SEMESTER_START_AUTUMN)
spring, autumn = min(start, start2), max(start, start2) spring = date(today.year, *settings.SITH_SEMESTER_START_SPRING)
if today > autumn: # autumn semester
if today >= autumn: # between 15/08 (included) and 31/12 -> autumn semester
return autumn return autumn
if today > spring: # spring semester if today >= spring: # between 15/02 (included) and 15/08 -> spring semester
return spring 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) start = get_start_of_semester(d)
if start.month <= 6:
return "P" + str(start.year)[-2:] if (start.month, start.day) == settings.SITH_SEMESTER_START_AUTUMN:
else:
return "A" + str(start.year)[-2:] return "A" + str(start.year)[-2:]
return "P" + str(start.year)[-2:]
def file_exist(path): def file_exist(path):

View File

@ -15,7 +15,7 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import Tuple from typing import Tuple, Optional
from django.db import models from django.db import models
from django.db.models import F, Value, Sum, QuerySet, OuterRef, Exists from django.db.models import F, Value, Sum, QuerySet, OuterRef, Exists
@ -536,7 +536,7 @@ class Counter(models.Model):
.order_by("-perm_sum") .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 Return a QuerySet querying the money spent by customers of this counter
since the specified date, ordered by descending amount of money spent. 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 nickname of the customer
- the amount of money spent by the customer - the amount of money spent by the customer
""" """
if since is None:
since = get_start_of_semester()
return ( return (
self.sellings.filter(date__gte=since) self.sellings.filter(date__gte=since)
.annotate( .annotate(
@ -557,7 +559,8 @@ class Counter(models.Model):
) )
.annotate(nickname=F("customer__user__nick_name")) .annotate(nickname=F("customer__user__nick_name"))
.annotate(promo=F("customer__user__promo")) .annotate(promo=F("customer__user__promo"))
.values("customer__user", "name", "nickname") .annotate(user=F("customer__user"))
.values("user", "promo", "name", "nickname")
.annotate( .annotate(
selling_sum=Sum( selling_sum=Sum(
F("unit_price") * F("quantity"), output_field=CurrencyField() F("unit_price") * F("quantity"), output_field=CurrencyField()
@ -567,15 +570,17 @@ class Counter(models.Model):
.order_by("-selling_sum") .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 Compute and return the total turnover of this counter
since the date specified in parameter (by default, since the start of the current since the date specified in parameter (by default, since the start of the current
semester) semester)
:param since: timestamp from which to perform the calculation :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 :return: Total revenue earned at this counter
""" """
if since is None:
since = get_start_of_semester()
if isinstance(since, date): if isinstance(since, date):
since = datetime.combine(since, datetime.min.time()) since = datetime.combine(since, datetime.min.time())
total = self.sellings.filter(date__gte=since).aggregate( total = self.sellings.filter(date__gte=since).aggregate(

View File

@ -11,7 +11,9 @@
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} stats{% endtrans %}</h3>
<h4>{% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %}</h4> <h4>
{% trans counter_name=counter.name %}Top 100 {{ counter_name }}{% endtrans %} ({{ current_semester }})
</h4>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -35,7 +37,9 @@
</tbody> </tbody>
</table> </table>
<h4>{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }}{% endtrans %}</h4> <h4>
{% trans counter_name=counter.name %}Top 100 barman {{ counter_name }}{% endtrans %} ({{ current_semester }})
</h4>
<table> <table>
<thead> <thead>
<tr> <tr>

View File

@ -13,6 +13,7 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from datetime import date, timedelta
import json import json
import re import re
import string import string
@ -322,42 +323,49 @@ class CounterStatsTest(TestCase):
Test the result of Counter.get_top_customers() is correct Test the result of Counter.get_top_customers() is correct
""" """
top = iter(self.counter.get_top_customers()) top = iter(self.counter.get_top_customers())
self.assertEqual( expected_results = [
next(top),
{ {
"customer__user": self.sli.id, "user": self.sli.id,
"name": f"{self.sli.first_name} {self.sli.last_name}", "name": f"{self.sli.first_name} {self.sli.last_name}",
"promo": self.sli.promo,
"nickname": self.sli.nick_name, "nickname": self.sli.nick_name,
"selling_sum": 2000, "selling_sum": 2000,
}, },
)
self.assertEqual(
next(top),
{ {
"customer__user": self.skia.id, "user": self.skia.id,
"name": f"{self.skia.first_name} {self.skia.last_name}", "name": f"{self.skia.first_name} {self.skia.last_name}",
"promo": self.skia.promo,
"nickname": self.skia.nick_name, "nickname": self.skia.nick_name,
"selling_sum": 1000, "selling_sum": 1000,
}, },
)
self.assertEqual(
next(top),
{ {
"customer__user": self.krophil.id, "user": self.krophil.id,
"name": f"{self.krophil.first_name} {self.krophil.last_name}", "name": f"{self.krophil.first_name} {self.krophil.last_name}",
"promo": self.krophil.promo,
"nickname": self.krophil.nick_name, "nickname": self.krophil.nick_name,
"selling_sum": 100, "selling_sum": 100,
}, },
)
self.assertEqual(
next(top),
{ {
"customer__user": self.root.id, "user": self.root.id,
"name": f"{self.root.first_name} {self.root.last_name}", "name": f"{self.root.first_name} {self.root.last_name}",
"promo": self.root.promo,
"nickname": self.root.nick_name, "nickname": self.root.nick_name,
"selling_sum": 2, "selling_sum": 2,
}, },
) ]
for result in expected_results:
self.assertEqual(
next(top),
{
"user": result["user"],
"name": result["name"],
"promo": result["promo"],
"nickname": result["nickname"],
"selling_sum": result["selling_sum"],
},
)
self.assertIsNone(next(top, None)) self.assertIsNone(next(top, None))

View File

@ -48,7 +48,7 @@ import pytz
from datetime import timedelta, datetime from datetime import timedelta, datetime
from http import HTTPStatus from http import HTTPStatus
from core.utils import get_start_of_semester from core.utils import get_start_of_semester, get_semester_code
from core.views import CanViewMixin, TabedViewMixin, CanEditMixin from core.views import CanViewMixin, TabedViewMixin, CanEditMixin
from core.views.forms import LoginForm from core.views.forms import LoginForm
from core.models import User from core.models import User
@ -1354,13 +1354,14 @@ class CounterStatView(DetailView, CounterAdminMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Add stats to the context""" """Add stats to the context"""
counter = self.object counter: Counter = self.object
semester_start = get_start_of_semester() semester_start = get_start_of_semester()
office_hours = counter.get_top_barmen() office_hours = counter.get_top_barmen()
kwargs = super(CounterStatView, self).get_context_data(**kwargs) kwargs = super(CounterStatView, self).get_context_data(**kwargs)
kwargs.update( kwargs.update(
{ {
"counter": counter, "counter": counter,
"current_semester": get_semester_code(),
"total_sellings": counter.get_total_sales(since=semester_start), "total_sellings": counter.get_total_sales(since=semester_start),
"top_customers": counter.get_top_customers(since=semester_start)[:100], "top_customers": counter.get_top_customers(since=semester_start)[:100],
"top_barman": office_hours[:100], "top_barman": office_hours[:100],

18
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]] [[package]]
name = "alabaster" name = "alabaster"
@ -550,6 +550,20 @@ files = [
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
] ]
[[package]]
name = "freezegun"
version = "1.2.2"
description = "Let your Python tests travel through time"
optional = false
python-versions = ">=3.6"
files = [
{file = "freezegun-1.2.2-py3-none-any.whl", hash = "sha256:ea1b963b993cb9ea195adbd893a48d573fda951b0da64f60883d7e988b606c9f"},
{file = "freezegun-1.2.2.tar.gz", hash = "sha256:cd22d1ba06941384410cd967d8a99d5ae2442f57dfafeff2fda5de8dc5c05446"},
]
[package.dependencies]
python-dateutil = ">=2.7"
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.4" version = "3.4"
@ -1558,4 +1572,4 @@ testing = ["coverage"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "9d38fb0dd50ef0a154bd6690afcb24be9fbdb2adbba1c4762b1ed0cdb9508eb2" content-hash = "62519616aff5a472dac3dd8071a6404b1ee8eab12a197af717a0520f7ded0331"

View File

@ -59,6 +59,7 @@ testing = ["coverage"]
docs = ["Sphinx", "sphinx-rtd-theme", "sphinx-copybutton"] docs = ["Sphinx", "sphinx-rtd-theme", "sphinx-copybutton"]
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
freezegun = "^1.2.2" # used to test time-dependent code
django-debug-toolbar = "^4.0.0" django-debug-toolbar = "^4.0.0"
ipython = "^7.28.0" ipython = "^7.28.0"
black = "^23.3.0" black = "^23.3.0"

View File

@ -327,7 +327,8 @@ SITH_CLUB_ROOT_PAGE = "clubs"
# Define the date in the year serving as reference for the subscriptions calendar # Define the date in the year serving as reference for the subscriptions calendar
# (month, day) # (month, day)
SITH_START_DATE = (8, 15) # 15th August SITH_SEMESTER_START_AUTUMN = (8, 15) # 15 August
SITH_SEMESTER_START_SPRING = (2, 15) # 15 February
# Used to determine the valid promos # Used to determine the valid promos
SITH_SCHOOL_START_YEAR = 1999 SITH_SCHOOL_START_YEAR = 1999

View File

@ -114,12 +114,12 @@ class Subscription(models.Model):
return "No user - " + str(self.pk) return "No user - " + str(self.pk)
@staticmethod @staticmethod
def compute_start(d=None, duration=1, user=None): def compute_start(d: date = None, duration: int = 1, user: User = None) -> date:
""" """
This function computes the start date of the subscription with respect to the given date (default is today), This function computes the start date of the subscription with respect to the given date (default is today),
and the start date given in settings.SITH_START_DATE. and the start date given in settings.SITH_SEMESTER_START_AUTUMN.
It takes the nearest past start date. It takes the nearest past start date.
Exemples: with SITH_START_DATE = (8, 15) Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
Today -> Start date Today -> Start date
2015-03-17 -> 2015-02-15 2015-03-17 -> 2015-02-15
2015-01-11 -> 2014-08-15 2015-01-11 -> 2014-08-15
@ -135,9 +135,9 @@ class Subscription(models.Model):
return get_start_of_semester(d) return get_start_of_semester(d)
@staticmethod @staticmethod
def compute_end(duration, start=None, user=None): def compute_end(duration: int, start: date = None, user: User = None) -> date:
""" """
This function compute the end date of the subscription given a start date and a duration in number of semestre This function compute the end date of the subscription given a start date and a duration in number of semester
Exemple: Exemple:
Start - Duration -> End date Start - Duration -> End date
2015-09-18 - 1 -> 2016-03-18 2015-09-18 - 1 -> 2016-03-18
@ -153,7 +153,7 @@ class Subscription(models.Model):
days=math.ceil((6 * duration - round(6 * duration)) * 30), days=math.ceil((6 * duration - round(6 * duration)) * 30),
) )
def can_be_edited_by(self, user): def can_be_edited_by(self, user: User):
return user.is_board_member or user.is_root return user.is_board_member or user.is_root
def is_valid_now(self): def is_valid_now(self):

View File

@ -31,7 +31,7 @@ from django.core.exceptions import ValidationError
from datetime import timedelta, date from datetime import timedelta, date
from core.models import User from core.models import User
from core.utils import get_start_of_semester, get_semester from core.utils import get_start_of_semester, get_semester_code
from club.models import Club from club.models import Club
@ -164,14 +164,14 @@ class TrombiUser(models.Model):
if m.description: if m.description:
role += " (%s)" % m.description role += " (%s)" % m.description
if m.end_date: if m.end_date:
end_date = get_semester(m.end_date) end_date = get_semester_code(m.end_date)
else: else:
end_date = "" end_date = ""
TrombiClubMembership( TrombiClubMembership(
user=self, user=self,
club=str(m.club), club=str(m.club),
role=role[:64], role=role[:64],
start=get_semester(m.start_date), start=get_semester_code(m.start_date),
end=end_date, end=end_date,
).save() ).save()