mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-21 13:43:20 +00:00
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:
parent
544b0248b2
commit
38295e591d
@ -1031,7 +1031,7 @@ thead {
|
||||
}
|
||||
|
||||
tbody > tr {
|
||||
&:nth-child(even) {
|
||||
&:nth-child(even):not(.highlight) {
|
||||
background: $primary-neutral-light-color;
|
||||
}
|
||||
&.clickable:hover {
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
@ -11,7 +11,9 @@
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -35,7 +37,9 @@
|
||||
</tbody>
|
||||
</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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -13,6 +13,7 @@
|
||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||
#
|
||||
#
|
||||
from datetime import date, timedelta
|
||||
import json
|
||||
import re
|
||||
import string
|
||||
@ -322,42 +323,49 @@ class CounterStatsTest(TestCase):
|
||||
Test the result of Counter.get_top_customers() is correct
|
||||
"""
|
||||
top = iter(self.counter.get_top_customers())
|
||||
self.assertEqual(
|
||||
next(top),
|
||||
expected_results = [
|
||||
{
|
||||
"customer__user": self.sli.id,
|
||||
"user": self.sli.id,
|
||||
"name": f"{self.sli.first_name} {self.sli.last_name}",
|
||||
"promo": self.sli.promo,
|
||||
"nickname": self.sli.nick_name,
|
||||
"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}",
|
||||
"promo": self.skia.promo,
|
||||
"nickname": self.skia.nick_name,
|
||||
"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}",
|
||||
"promo": self.krophil.promo,
|
||||
"nickname": self.krophil.nick_name,
|
||||
"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}",
|
||||
"promo": self.root.promo,
|
||||
"nickname": self.root.nick_name,
|
||||
"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))
|
||||
|
||||
|
||||
|
@ -48,7 +48,7 @@ import pytz
|
||||
from datetime import timedelta, datetime
|
||||
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.forms import LoginForm
|
||||
from core.models import User
|
||||
@ -1354,13 +1354,14 @@ class CounterStatView(DetailView, CounterAdminMixin):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add stats to the context"""
|
||||
counter = self.object
|
||||
counter: Counter = self.object
|
||||
semester_start = get_start_of_semester()
|
||||
office_hours = counter.get_top_barmen()
|
||||
kwargs = super(CounterStatView, self).get_context_data(**kwargs)
|
||||
kwargs.update(
|
||||
{
|
||||
"counter": counter,
|
||||
"current_semester": get_semester_code(),
|
||||
"total_sellings": counter.get_total_sales(since=semester_start),
|
||||
"top_customers": counter.get_top_customers(since=semester_start)[:100],
|
||||
"top_barman": office_hours[:100],
|
||||
|
18
poetry.lock
generated
18
poetry.lock
generated
@ -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]]
|
||||
name = "alabaster"
|
||||
@ -550,6 +550,20 @@ files = [
|
||||
{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]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
@ -1558,4 +1572,4 @@ testing = ["coverage"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "9d38fb0dd50ef0a154bd6690afcb24be9fbdb2adbba1c4762b1ed0cdb9508eb2"
|
||||
content-hash = "62519616aff5a472dac3dd8071a6404b1ee8eab12a197af717a0520f7ded0331"
|
||||
|
@ -59,6 +59,7 @@ testing = ["coverage"]
|
||||
docs = ["Sphinx", "sphinx-rtd-theme", "sphinx-copybutton"]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
freezegun = "^1.2.2" # used to test time-dependent code
|
||||
django-debug-toolbar = "^4.0.0"
|
||||
ipython = "^7.28.0"
|
||||
black = "^23.3.0"
|
||||
|
@ -327,7 +327,8 @@ SITH_CLUB_ROOT_PAGE = "clubs"
|
||||
|
||||
# Define the date in the year serving as reference for the subscriptions calendar
|
||||
# (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
|
||||
SITH_SCHOOL_START_YEAR = 1999
|
||||
|
@ -114,12 +114,12 @@ class Subscription(models.Model):
|
||||
return "No user - " + str(self.pk)
|
||||
|
||||
@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),
|
||||
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.
|
||||
Exemples: with SITH_START_DATE = (8, 15)
|
||||
Exemples: with SITH_SEMESTER_START_AUTUMN = (8, 15)
|
||||
Today -> Start date
|
||||
2015-03-17 -> 2015-02-15
|
||||
2015-01-11 -> 2014-08-15
|
||||
@ -135,9 +135,9 @@ class Subscription(models.Model):
|
||||
return get_start_of_semester(d)
|
||||
|
||||
@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:
|
||||
Start - Duration -> End date
|
||||
2015-09-18 - 1 -> 2016-03-18
|
||||
@ -153,7 +153,7 @@ class Subscription(models.Model):
|
||||
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
|
||||
|
||||
def is_valid_now(self):
|
||||
|
@ -31,7 +31,7 @@ from django.core.exceptions import ValidationError
|
||||
from datetime import timedelta, date
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -164,14 +164,14 @@ class TrombiUser(models.Model):
|
||||
if m.description:
|
||||
role += " (%s)" % m.description
|
||||
if m.end_date:
|
||||
end_date = get_semester(m.end_date)
|
||||
end_date = get_semester_code(m.end_date)
|
||||
else:
|
||||
end_date = ""
|
||||
TrombiClubMembership(
|
||||
user=self,
|
||||
club=str(m.club),
|
||||
role=role[:64],
|
||||
start=get_semester(m.start_date),
|
||||
start=get_semester_code(m.start_date),
|
||||
end=end_date,
|
||||
).save()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user