Compare commits

..
Author SHA1 Message Date
imperosol 111fca85db fix: user profile pict deletion cancel 2026-09-07 16:51:22 +02:00
83 changed files with 1028 additions and 1233 deletions
+1 -1
View File
@@ -1 +1 @@
3.14
3.12
+2 -6
View File
@@ -1,15 +1,11 @@
from typing import TYPE_CHECKING
from django.contrib import admin, messages
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from api.hashers import generate_key
from api.models import ApiClient, ApiKey
if TYPE_CHECKING:
from django.db.models import QuerySet
from django.http import HttpRequest
@admin.register(ApiClient)
class ApiClientAdmin(admin.ModelAdmin):
+1 -5
View File
@@ -1,13 +1,9 @@
from typing import TYPE_CHECKING
from django.http import HttpRequest
from ninja.security import APIKeyHeader
from api.hashers import get_hasher
from api.models import ApiClient, ApiKey
if TYPE_CHECKING:
from django.http import HttpRequest
class ApiKeyAuth(APIKeyHeader):
"""Authentication through client api keys."""
+4 -6
View File
@@ -39,16 +39,14 @@ Example:
import operator
from functools import reduce
from typing import TYPE_CHECKING, Any, Callable
from typing import Any, Callable
from ninja_extra.permissions import BasePermission
from counter.utils import is_logged_in_counter
if TYPE_CHECKING:
from django.contrib.auth.models import Permission
from django.http import HttpRequest
from ninja_extra import ControllerBase
from ninja_extra.permissions import BasePermission
from counter.utils import is_logged_in_counter
class IsInGroup(BasePermission):
+4 -8
View File
@@ -12,16 +12,12 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from typing import TYPE_CHECKING
from django.contrib import admin
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
if TYPE_CHECKING:
from django.forms.models import ModelForm
from django.http import HttpRequest
from club.models import Club, ClubLink, ClubRole, LinkType, Membership
@admin.register(Club)
class ClubAdmin(admin.ModelAdmin):
@@ -50,8 +46,8 @@ class ClubAdmin(admin.ModelAdmin):
@admin.register(ClubRole)
class ClubRoleAdmin(admin.ModelAdmin):
list_display = ("name", "club", "is_board", "is_presidency")
search_fields = ("name", "club__name")
autocomplete_fields = ("club", "linked_groups")
search_fields = ("name",)
autocomplete_fields = ("club",)
list_select_related = ("club",)
list_filter = (
"is_board",
-7
View File
@@ -479,13 +479,6 @@ class ClubRoleCreateForm(forms.ModelForm):
class ClubRoleBaseFormSet(forms.BaseInlineFormSet):
ordering_widget = forms.HiddenInput()
def __init__(self, *args, queryset=None, **kwargs):
if queryset is None:
queryset = self.model._default_manager
super().__init__(
*args, queryset=queryset.prefetch_related("linked_groups"), **kwargs
)
ClubRoleFormSet = forms.inlineformset_factory(
Club,
@@ -1,16 +1,12 @@
# Generated by Django 4.2.16 on 2024-11-20 17:08
from typing import TYPE_CHECKING
import django.db.models.deletion
import django.db.models.functions.datetime
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Q
from django.utils.timezone import localdate
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
# Before the club role rework, the maximum free role
# was the hardcoded highest non-board role
MAXIMUM_FREE_ROLE = 1
@@ -1,13 +1,9 @@
# Generated by Django 5.2.3 on 2025-06-21 21:59
from typing import TYPE_CHECKING
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import Case, When
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
from django.db.models import Case, When
PRESIDENCY_ROLES = [10, 9]
MAXIMUM_FREE_ROLE = 1
@@ -1,26 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-01 14:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("club", "0017_linktype_clublink"),
("core", "0050_alter_sithfile_moderator"),
]
operations = [
migrations.AddField(
model_name="clubrole",
name="linked_groups",
field=models.ManyToManyField(
help_text=(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="club_roles",
to="core.group",
verbose_name="Linked groups",
),
),
]
+11 -32
View File
@@ -23,8 +23,6 @@
#
from __future__ import annotations
import operator
from functools import reduce
from typing import Iterable, Self
from django.conf import settings
@@ -284,15 +282,6 @@ class ClubRole(OrderedModel):
"If the role is inactive, people joining the club won't be able to get it."
),
)
linked_groups = models.ManyToManyField(
Group,
verbose_name=_("Linked groups"),
help_text=_(
"Groups that are automatically given or removed "
"to user receiving or losing this club role"
),
related_name="linked_roles",
)
order_with_respect_to = "club"
@@ -545,7 +534,7 @@ class Membership(models.Model):
def _remove_club_groups(
memberships: Iterable[Membership],
) -> tuple[int, dict[str, int]]:
"""Remove users of those memberships from the club and club role groups.
"""Remove users of those memberships from the club groups.
For example, if a user is in the Troll club board,
he is in the board group and the members group of the Troll.
@@ -564,19 +553,15 @@ class Membership(models.Model):
clubs = {m.club_id for m in memberships}
users = {m.user_id for m in memberships}
groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs))
role_groups = [
Q(user_id=m.user_id, group__linked_roles=m.role_id) for m in memberships
]
return User.groups.through.objects.filter(
(Q(group__in=groups) & Q(user__in=users))
| reduce(operator.or_, role_groups)
Q(group__in=groups) & Q(user__in=users)
).delete()
@staticmethod
def _add_club_groups(
memberships: Iterable[Membership],
) -> list[User.groups.through]:
"""Add users of those memberships to the club and club role groups.
"""Add users of those memberships to the club groups.
For example, if a user just joined the Troll club board,
he will be added in both the members group and the board group
@@ -597,40 +582,34 @@ class Membership(models.Model):
memberships = [m for m in memberships if m.end_date is None]
if not memberships:
return []
nb_prefetched = sum(
1 for m in memberships if not hasattr(m, "club") or not hasattr(m, "role")
)
if nb_prefetched > 1:
if sum(1 for m in memberships if not hasattr(m, "club")) > 1:
# if more than one membership hasn't its `club` attribute set
# it's less expensive to reload the whole query with
# a select_related than perform a distinct query
# to fetch each club.
ids = {m.id for m in memberships}
memberships = list(
Membership.objects.filter(id__in=ids)
.select_related("club", "role")
.prefetch_related("role__linked_groups")
Membership.objects.filter(id__in=ids).select_related("club")
)
groups = []
club_groups = []
for membership in memberships:
groups.append(
club_groups.append(
User.groups.through(
user_id=membership.user_id,
group_id=membership.club.members_group_id,
)
)
if membership.role.is_board:
groups.append(
club_groups.append(
User.groups.through(
user_id=membership.user_id,
group_id=membership.club.board_group_id,
)
)
groups.extend(
User.groups.through(user_id=membership.user_id, group_id=g.id)
for g in membership.role.linked_groups.all()
return User.groups.through.objects.bulk_create(
club_groups, ignore_conflicts=True
)
return User.groups.through.objects.bulk_create(groups, ignore_conflicts=True)
class Mailing(models.Model):
-14
View File
@@ -49,20 +49,6 @@
{{ subform.is_active.help_text }}
</span>
</div>
{% set groups = subform.instance.linked_groups.all()|list %}
{% if groups %}
<div>
<p>
<strong>{% trans %}Linked groups : {% endtrans %}</strong>
{{ groups|map(attribute="name")|join(", ") }}
</p>
<p class="helptext">
{% trans trimmed %}
Users receiving this role will also be assigned to those groups
{% endtrans %}
</p>
</div>
{% endif %}
</div>
</details>
</div>
+1 -4
View File
@@ -1,9 +1,9 @@
from datetime import timedelta
from typing import TYPE_CHECKING
import pytest
from django.conf import settings
from django.db import ProgrammingError
from django.test import Client
from django.urls import reverse
from django.utils.timezone import localdate
from model_bakery import baker
@@ -14,9 +14,6 @@ from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
from core.models import User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
def test_club_queryset_having_board_member():
+1 -4
View File
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING
from collections.abc import Callable
import pytest
from django.contrib.auth.models import Permission
@@ -14,9 +14,6 @@ from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User
if TYPE_CHECKING:
from collections.abc import Callable
def make_club():
# unittest-style tests cannot use fixture, so we create a function
+1 -5
View File
@@ -1,6 +1,5 @@
from typing import TYPE_CHECKING
import pytest
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
@@ -8,9 +7,6 @@ from pytest_django.asserts import assertRedirects
from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
def test_club_board_member_cannot_edit_club_properties(client: Client):
+2 -28
View File
@@ -1,6 +1,6 @@
import itertools
from collections.abc import Callable
from datetime import timedelta
from typing import TYPE_CHECKING
import pytest
from bs4 import BeautifulSoup
@@ -17,10 +17,7 @@ from club.forms import ClubAddMemberForm, JoinClubForm
from club.models import Club, ClubRole, Membership
from club.tests.base import TestClub
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, Group, User
if TYPE_CHECKING:
from collections.abc import Callable
from core.models import AnonymousUser, User
class TestMembershipQuerySet(TestClub):
@@ -503,29 +500,6 @@ class TestMembership(TestClub):
assert self.subscriber.groups.contains(self.club.members_group)
assert self.subscriber.groups.contains(self.club.board_group)
def test_add_to_club_role_group(self):
groups = baker.make(Group, _quantity=4)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:3])
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
assert set(self.subscriber.groups.all()) == {
*groups[:3],
self.club.board_group,
self.club.members_group,
}
def test_remove_from_club_role_group(self):
groups = baker.make(Group, _quantity=3)
baker.make(
Membership, club=self.club, user=self.subscriber, role=self.board_role
)
self.subscriber.groups.set(groups[:1])
self.board_role.linked_groups.set(groups[1:])
self.subscriber.memberships.update(end_date=localdate())
assert set(self.subscriber.groups.all()) == {groups[0]}
def test_change_position_in_club(self):
"""Test that when moving from board to members, club group change"""
membership = baker.make(
+1 -5
View File
@@ -1,8 +1,7 @@
from typing import TYPE_CHECKING
import pytest
from aemark import markdown
from bs4 import BeautifulSoup
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertHTMLEqual, assertRedirects
@@ -11,9 +10,6 @@ from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user
from core.models import PageRev, User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
def test_page_display_on_club_main_page(client: Client):
+1 -5
View File
@@ -1,6 +1,5 @@
from typing import TYPE_CHECKING
import pytest
from django.test import Client
from django.urls import reverse
from model_bakery import baker
@@ -8,9 +7,6 @@ from club.models import Club
from com.models import Poster
from core.baker_recipes import subscriber_user
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
@pytest.mark.parametrize("route_url", ["club:poster_list", "club:poster_create"])
+1 -4
View File
@@ -1,8 +1,8 @@
import csv
import itertools
from typing import TYPE_CHECKING
import pytest
from django.test import Client
from django.urls import reverse
from model_bakery import baker
@@ -12,9 +12,6 @@ from core.models import User
from counter.baker_recipes import product_recipe, sale_recipe
from counter.models import Counter, Customer, Product, Selling
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
def test_sales_page_doesnt_crash(client: Client):
+1 -4
View File
@@ -1,5 +1,4 @@
from datetime import date
from typing import TYPE_CHECKING
from dateutil.relativedelta import relativedelta
from django import forms
@@ -10,13 +9,11 @@ from django.utils.translation import gettext_lazy as _
from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub
from com.models import News, NewsDate, Poster
from core.models import User
from core.utils import get_end_of_semester
from core.views.forms import SelectDateTime
from core.views.widgets.markdown import MarkdownInput
if TYPE_CHECKING:
from core.models import User
class PosterForm(forms.ModelForm):
class Meta:
+2 -7
View File
@@ -1,10 +1,11 @@
from typing import TYPE_CHECKING
from pathlib import Path
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.sites.models import Site
from django.contrib.syndication.views import add_domain
from django.db.models import Count, OuterRef, QuerySet, Subquery
from django.http import HttpRequest
from django.urls import reverse
from django.utils import timezone
from ical.calendar import Calendar
@@ -13,12 +14,6 @@ from ical.event import Event
from ical.types import Frequency, Recur
from com.models import News, NewsDate
if TYPE_CHECKING:
from pathlib import Path
from django.http import HttpRequest
from core.models import User
+1 -4
View File
@@ -1,11 +1,11 @@
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
from aemark import markdown
from django.conf import settings
from django.contrib.auth.models import Permission
from django.http import HttpResponse
from django.test import Client, TestCase
from django.urls import reverse
from django.utils.timezone import now
@@ -16,9 +16,6 @@ from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate
from core.models import User
if TYPE_CHECKING:
from django.http import HttpResponse
def accel_redirect_to_file(response: HttpResponse) -> Path | None:
redirect = Path(response.headers.get("X-Accel-Redirect", ""))
@@ -1,12 +1,8 @@
# Generated by Django 4.2.17 on 2025-01-04 16:42
from typing import TYPE_CHECKING
from django.db import migrations
from django.db.models import F
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
from django.db.models import F
def invert_is_manually_manageable(apps: StateApps, schema_editor):
@@ -1,13 +1,9 @@
# Generated by Django 4.2.17 on 2024-12-31 13:30
from typing import TYPE_CHECKING
import django.contrib.auth.models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
@@ -1,14 +1,10 @@
# Generated by Django 5.2.12 on 2026-05-01 08:59
from typing import TYPE_CHECKING
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.db.models import F
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
from django.db.models import F
def set_updated_at(apps: StateApps, schema_editor):
-2
View File
@@ -62,8 +62,6 @@ document.body.addEventListener(
},
);
htmx.config.transitions = true;
Object.assign(window, { htmx });
/**
@@ -1,54 +0,0 @@
{% extends "core/file.jinja" %}
{% block title %}
{% trans %}Delete confirmation{% endtrans %}
{% endblock %}
{% if is_fragment %}
{# Don't display tabs and errors #}
{% block tabs %}
{% endblock %}
{% block errors %}
{% endblock %}
{% endif %}
{% block file %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
{% if next %}
{% set action = current + "?next=" + next %}
{% else %}
{% set action = current %}
{% endif %}
<form action="{{ action }}" method="post">
{% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<button
{% if is_fragment %}
hx-post="{{ action }}"
hx-target="#content"
hx-swap="outerHtml"
{% endif %}
>{% trans %}Confirm{% endtrans %}</button>
<button
{% if is_fragment %}
hx-get="{{ previous }}"
hx-target="#content"
hx-swap="outerHtml"
{% else %}
action="window.history.back()"
{% endif %}
>{% trans %}Cancel{% endtrans %}</button>
</form>
{% endblock %}
+1 -3
View File
@@ -20,14 +20,12 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from typing import TYPE_CHECKING, Callable
from typing import Callable
import honeypot.templatetags.honeypot as honeypot_filters
from django.template.loader import render_to_string
from jinja2 import Environment, nodes
from jinja2.ext import Extension
if TYPE_CHECKING:
from jinja2.parser import Parser
+1 -4
View File
@@ -23,18 +23,15 @@
#
import datetime
from typing import TYPE_CHECKING
import phonenumbers
from aemark import markdown as md
from django import template
from django.forms import BoundField
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
if TYPE_CHECKING:
from django.forms import BoundField
register = template.Library()
+1 -4
View File
@@ -1,5 +1,4 @@
from datetime import timedelta
from typing import TYPE_CHECKING
import freezegun
import pytest
@@ -7,6 +6,7 @@ from aemark import markdown
from bs4 import BeautifulSoup
from django.conf import settings
from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse
from django.utils.timezone import now
from model_bakery import baker
@@ -16,9 +16,6 @@ from club.models import Club, Membership
from core.baker_recipes import board_user, subscriber_user
from core.models import AnonymousUser, Page, PageRev, User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
class TestEditPage:
+6 -22
View File
@@ -21,7 +21,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from django.urls import path, re_path, register_converter
from django.urls import path, register_converter
from django.views.generic import RedirectView
from com.views import NewsListView
@@ -193,27 +193,11 @@ urlpatterns = [
name="user_gift_delete",
),
# File views
re_path(r"^file/$", FileListView.as_view(), name="file_list"),
re_path(
r"^file/(?P<file_id>[0-9]+)/$",
FileView.as_view(),
name="file_detail",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/edit/$",
FileEditView.as_view(),
name="file_edit",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/prop/$",
FileEditPropView.as_view(),
name="file_prop",
),
re_path(
r"^file/(?P<file_id>[0-9]+)/delete/$",
FileDeleteView.as_view(),
name="file_delete",
),
path(r"file/", FileListView.as_view(), name="file_list"),
path("file/<int:file_id>/", FileView.as_view(), name="file_detail"),
path("file/<int:file_id>/edit/", FileEditView.as_view(), name="file_edit"),
path("file/<int:file_id>/prop/", FileEditPropView.as_view(), name="file_prop"),
path("file/<int:file_id>/delete/", FileDeleteView.as_view(), name="file_delete"),
path("file/moderation/", FileModerationView.as_view(), name="file_moderation"),
path(
"file/<int:file_id>/moderate/", FileModerateView.as_view(), name="file_moderate"
+3 -5
View File
@@ -17,17 +17,15 @@ from datetime import date, timedelta
# Image utils
from io import BytesIO
from typing import TYPE_CHECKING, Final
from typing import Final
import PIL
from django.conf import settings
from django.core.files.base import ContentFile
from django.utils.timezone import localdate
from PIL.Image import Image, Resampling
if TYPE_CHECKING:
from django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest
from django.utils.timezone import localdate
from PIL.Image import Image, Resampling
RED_PIXEL_PNG: Final[bytes] = (
b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
+5 -23
View File
@@ -13,7 +13,7 @@
#
#
import mimetypes
from typing import TYPE_CHECKING
from pathlib import Path
from urllib.parse import quote, urljoin
# This file contains all the views that concern the page model
@@ -48,9 +48,6 @@ from core.views.widgets.ajax_select import (
)
from counter.utils import is_logged_in_counter
if TYPE_CHECKING:
from pathlib import Path
def send_raw_file(path: Path) -> HttpResponse:
"""Send a file located in the MEDIA_ROOT
@@ -345,33 +342,18 @@ class FileView(CanViewMixin, DetailView, FormMixin):
return kwargs
class FileDeleteView(AllowFragment, CanEditPropMixin, DeleteView):
class FileDeleteView(CanEditPropMixin, DeleteView):
model = SithFile
pk_url_kwarg = "file_id"
template_name = "core/file_delete_confirm.jinja"
context_object_name = "file"
template_name = "core/delete_confirm.jinja"
def get_success_url(self):
self.object.file.delete() # Doing it here or overloading delete() is the same, so let's do it here
if "next" in self.request.GET:
return self.request.GET["next"]
if self.object.parent is None:
return reverse(
"core:file_list",
)
return reverse(
"core:file_detail",
kwargs={
"file_id": self.object.parent.id,
},
)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["next"] = self.request.GET.get("next", None)
kwargs["previous"] = self.request.GET.get("previous", None)
kwargs["current"] = self.request.path
return kwargs
return reverse("core:file_list")
return reverse("core:file_detail", kwargs={"file_id": self.object.parent.id})
class FileModerationView(AllowFragment, ListView):
+1 -4
View File
@@ -22,8 +22,8 @@
#
import re
from copy import copy
from datetime import date, datetime
from io import BytesIO
from typing import TYPE_CHECKING
from captcha.fields import CaptchaField
from django import forms
@@ -59,9 +59,6 @@ from core.views.widgets.ajax_select import (
)
from core.views.widgets.markdown import MarkdownInput
if TYPE_CHECKING:
from datetime import date, datetime
# Widgets
+2 -6
View File
@@ -22,11 +22,11 @@
#
#
from typing import TYPE_CHECKING
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import F
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import ListView, TemplateView
@@ -34,10 +34,6 @@ from club.models import Club
from core.models import Notification, User
from core.schemas import UserFilterSchema
if TYPE_CHECKING:
from django.db.models.query import QuerySet
from django.http import HttpRequest
class NotificationList(LoginRequiredMixin, ListView):
model = Notification
+3 -5
View File
@@ -1,16 +1,14 @@
import copy
import inspect
from typing import TYPE_CHECKING, Any, ClassVar, LiteralString, Protocol, Unpack
from typing import Any, ClassVar, LiteralString, Protocol, Unpack
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string
from django.utils.safestring import SafeString
from django.views import View
from django.views.generic.base import ContextMixin, TemplateResponseMixin
if TYPE_CHECKING:
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import SafeString
class TabedViewMixin(View):
"""Basic functions for displaying tabs in the template."""
+1 -4
View File
@@ -27,7 +27,6 @@ from datetime import timedelta
# This file contains all the views that concern the user model
from operator import itemgetter
from smtplib import SMTPException
from typing import TYPE_CHECKING
from django.contrib import messages
from django.contrib.auth import login, views
@@ -44,6 +43,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import (
@@ -73,9 +73,6 @@ from counter.models import Refilling, Selling
from eboutic.models import Invoice
from trombi.views import UserTrombiForm
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class SithLoginView(views.LoginView):
"""The login View."""
+4 -7
View File
@@ -1,18 +1,15 @@
from typing import TYPE_CHECKING, Any
from collections.abc import Collection
from typing import Any
from django.contrib.staticfiles.storage import staticfiles_storage
from django.db.models import Model, QuerySet
from django.forms import Select, SelectMultiple
from ninja import ModelSchema
from pydantic import TypeAdapter
from core.models import Group, SithFile, User
from core.schemas import GroupSchema, SithFileSchema, UserProfileSchema
if TYPE_CHECKING:
from collections.abc import Collection
from django.db.models import Model, QuerySet
from ninja import ModelSchema
class AutoCompleteSelectMixin:
component_name = "autocomplete-select"
+2 -4
View File
@@ -3,7 +3,7 @@ import math
import uuid
from collections import defaultdict
from datetime import date, datetime, timezone
from typing import TYPE_CHECKING, ClassVar
from typing import ClassVar
from dateutil.relativedelta import relativedelta
from django import forms
@@ -11,6 +11,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef, Q
from django.forms import BaseModelFormSet
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -58,9 +59,6 @@ from counter.widgets.ajax_select import (
AutoCompleteSelectProduct,
)
if TYPE_CHECKING:
from django.http import HttpRequest
class BillingInfoForm(forms.ModelForm):
class Meta:
+1 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from operator import attrgetter
from typing import TYPE_CHECKING
from django.conf import settings
from django.core.mail import send_mass_mail
@@ -13,9 +13,6 @@ from django.utils.translation import gettext as _
from core.models import User, UserQuerySet
from counter.models import AccountDump, Counter, Customer, Product, Selling
if TYPE_CHECKING:
from collections.abc import Iterable
class Command(BaseCommand):
"""Effectively dump the inactive users.
+2 -3
View File
@@ -1,14 +1,13 @@
from typing import TYPE_CHECKING, Callable
from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject
from core.models import User
from counter.models import Permanency
if TYPE_CHECKING:
from django.contrib.sessions.backends.base import SessionBase
from django.http import HttpRequest, HttpResponse
from core.models import User
SESSION_PERMANENCES_KEY = "permanence_ids"
@@ -1,13 +1,10 @@
# Generated by Django 4.2.17 on 2024-12-08 13:30
from operator import attrgetter
from typing import TYPE_CHECKING
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import Count
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
from django.db.models import Count
def delete_duplicates(apps: StateApps, schema_editor):
@@ -1,10 +1,6 @@
# Generated by Django 4.2.17 on 2024-12-15 17:53
from typing import TYPE_CHECKING
from django.db import migrations, models
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
@@ -1,12 +1,8 @@
# Generated by Django 5.2.8 on 2025-11-19 17:59
from typing import TYPE_CHECKING
from django.db import migrations, models
from django.db.models import Case, When
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
from django.db.models import Case, When
def migrate_selling_payment_method(apps: StateApps, schema_editor):
@@ -1,16 +1,13 @@
# Generated by Django 5.2.8 on 2026-02-10 15:40
from operator import attrgetter
from typing import TYPE_CHECKING
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import OuterRef, Subquery
from counter.models import Selling
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def apply_product_history_dates(apps: StateApps, schema_editor):
"""Approximate a posteriori the value of created_at and updated_at."""
+1 -5
View File
@@ -1,15 +1,11 @@
# Generated by Django 5.2.11 on 2026-02-18 13:30
from typing import TYPE_CHECKING
import django.db.models.deletion
from django.db import migrations, models
from django.db.migrations.state import StateApps
import counter.fields
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def migrate_prices(apps: StateApps, schema_editor):
Product = apps.get_model("counter", "Product")
+1 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from datetime import timedelta
from typing import TYPE_CHECKING
import freezegun
import pytest
@@ -17,9 +17,6 @@ from counter.management.commands.dump_warning_mail import Command as WarningComm
from counter.models import AccountDump, Customer, Refilling, Selling
from subscription.models import Subscription
if TYPE_CHECKING:
from collections.abc import Iterable
class TestAccountDump(TestCase):
@classmethod
+1 -4
View File
@@ -1,9 +1,9 @@
import json
from datetime import timedelta
from typing import TYPE_CHECKING
import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from django.utils.timezone import now
from django_celery_beat.models import ClockedSchedule
@@ -18,9 +18,6 @@ from counter.forms import (
)
from counter.models import Product, ProductType, ScheduledProductAction
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
def test_edit_product(client: Client):
+1 -4
View File
@@ -15,7 +15,6 @@
from dataclasses import asdict, dataclass
from datetime import timedelta
from decimal import Decimal
from typing import TYPE_CHECKING
import pytest
from bs4 import BeautifulSoup
@@ -23,6 +22,7 @@ from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.models import Permission, make_password
from django.contrib.messages import DEFAULT_LEVELS, get_messages
from django.http import HttpResponse
from django.shortcuts import resolve_url
from django.test import Client, TestCase
from django.urls import reverse
@@ -48,9 +48,6 @@ from counter.models import (
Selling,
)
if TYPE_CHECKING:
from django.http import HttpResponse
def set_age(user: User, age: int):
user.date_of_birth = localdate().replace(year=localdate().year - age)
+1 -4
View File
@@ -1,10 +1,10 @@
from datetime import date, datetime
from typing import TYPE_CHECKING
import pytest
from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.test import Client
from django.urls import reverse
from django.utils.timezone import now
from model_bakery import baker
@@ -16,9 +16,6 @@ from counter.baker_recipes import sale_recipe
from counter.forms import InvoiceCallForm
from counter.models import Customer, InvoiceCall, Selling
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
@pytest.mark.parametrize(
+1 -5
View File
@@ -1,7 +1,6 @@
from typing import TYPE_CHECKING
import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from model_bakery import baker, seq
@@ -9,9 +8,6 @@ from core.baker_recipes import board_user, subscriber_user
from core.models import Group, User
from counter.models import ProductType
if TYPE_CHECKING:
from django.test import Client
@pytest.fixture
def product_types(db) -> list[ProductType]:
+1 -4
View File
@@ -1,10 +1,7 @@
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from django.urls import resolve
if TYPE_CHECKING:
from django.http import HttpRequest
from django.urls import resolve
def is_logged_in_counter(request: HttpRequest) -> bool:
+1 -5
View File
@@ -13,8 +13,6 @@
#
#
from typing import TYPE_CHECKING
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -24,6 +22,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import BaseFormView, UpdateView
from core.models import User
from counter.forms import CashSummaryFormBase
from counter.models import (
CashRegisterSummary,
@@ -36,9 +35,6 @@ from counter.views.mixins import (
CounterTabsMixin,
)
if TYPE_CHECKING:
from core.models import User
class CashRegisterSummaryForm(forms.Form):
"""Provide the cash summary form."""
+3 -7
View File
@@ -14,7 +14,6 @@
#
import random
from collections import defaultdict
from typing import TYPE_CHECKING
from django.contrib import messages
from django.core.exceptions import PermissionDenied
@@ -23,11 +22,14 @@ from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.urls import reverse
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.views.generic import CreateView, FormView
from django.views.generic.detail import SingleObjectMixin
from ninja.main import HttpRequest
from core.auth.mixins import CanViewMixin
from core.models import User
from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BasketForm, RefillForm
from counter.models import (
@@ -42,12 +44,6 @@ from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormFragment
if TYPE_CHECKING:
from django.utils.safestring import SafeString
from ninja.main import HttpRequest
from core.models import User
def get_operator(request: HttpRequest, counter: Counter, customer: Customer) -> User:
if counter.type != "BAR":
+1 -4
View File
@@ -13,7 +13,6 @@
#
#
from datetime import timedelta
from typing import TYPE_CHECKING
from django.conf import settings
from django.core.exceptions import PermissionDenied
@@ -22,6 +21,7 @@ from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import SafeString
from django.views.decorators.http import require_POST
from django.views.generic import DetailView
from django.views.generic.detail import SingleObjectMixin
@@ -35,9 +35,6 @@ from counter.models import Counter, Permanency
from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
model = Counter
+1 -5
View File
@@ -13,12 +13,11 @@
#
#
from typing import TYPE_CHECKING
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _
from django.views.generic.edit import DeleteView, FormView
@@ -28,9 +27,6 @@ from counter.forms import StudentCardForm
from counter.models import Customer, StudentCard
from counter.utils import is_logged_in_counter
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class StudentCardDeleteView(DeleteView):
"""View used to delete a card from a user. This is a fragment view !"""
+2 -5
View File
@@ -1,12 +1,13 @@
import re
from datetime import datetime, timezone
from typing import TYPE_CHECKING
import freezegun
import pytest
from bs4 import BeautifulSoup
from django.conf import settings
from django.http import HttpResponse
from django.test import TestCase
from django.test.client import Client
from django.urls import reverse
from django.utils.timezone import localdate, now
from model_bakery import baker
@@ -30,10 +31,6 @@ from counter.models import (
from counter.tests.test_counter import BasketItem
from eboutic.models import Basket
if TYPE_CHECKING:
from django.http import HttpResponse
from django.test.client import Client
@pytest.mark.django_db
def test_get_eboutic():
+2 -4
View File
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING, Callable
from typing import Callable
import pytest
from django.test import Client
from django.urls import reverse
from model_bakery import baker
from pytest_django.asserts import assertRedirects
@@ -9,9 +10,6 @@ from core.baker_recipes import subscriber_user
from core.models import User
from counter.models import BillingInfo
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
class TestBillingInfo:
+1 -4
View File
@@ -1,7 +1,6 @@
from datetime import timedelta
from itertools import groupby, islice
from operator import attrgetter
from typing import TYPE_CHECKING
from django import forms
from django.conf import settings
@@ -14,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from club.forms import ClubRoleChoiceField
from club.models import ClubRole, Membership
from club.widgets.ajax_select import AutoCompleteSelectMultipleClub
from core.models import User
from core.views.forms import SelectDateTime
from core.views.widgets.ajax_select import (
AutoCompleteSelect,
@@ -23,9 +23,6 @@ from core.views.widgets.ajax_select import (
from core.views.widgets.markdown import MarkdownInput
from election.models import Candidature, Election, ElectionList, Role
if TYPE_CHECKING:
from core.models import User
class LimitedCheckboxField(forms.ModelMultipleChoiceField):
"""A `ModelMultipleChoiceField`, with a max limit of selectable inputs."""
+1 -5
View File
@@ -13,19 +13,15 @@
#
#
from typing import TYPE_CHECKING
import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from pytest_django.asserts import assertRedirects
from core.models import User
from forum.models import Forum, ForumMessage, ForumTopic
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
class TestTopicCreation:
-20
View File
@@ -260,18 +260,6 @@ msgstr ""
"Si ce rôle est inactif, il ne pourra pas être attribué aux gens qui "
"rejoignent le club."
#: club/models.py
msgid "Linked groups"
msgstr "Groupes liés"
#: club/models.py
msgid ""
"Groups that are automatically given or removed to user receiving or losing "
"this club role"
msgstr ""
"Les groupes qui sont automatiquement donnés ou retirés quand l'utilisateur "
"reçoit ou perd ce rôle de club"
#: club/models.py election/models.py
msgid "club role"
msgstr "rôle de club"
@@ -497,14 +485,6 @@ msgstr "Du"
msgid "To"
msgstr "Au"
#: club/templates/club/club_roles.jinja
msgid "Linked groups : "
msgstr "Groupes liés : "
#: club/templates/club/club_roles.jinja
msgid "Users receiving this role will also be assigned to those groups"
msgstr "Les utilisateurs recevant ce rôle seront aussi assignés à ces groupes"
#: club/templates/club/club_roles.jinja
msgid ""
"Roles give rights on the club. Higher roles grant more rights, and the "
+2 -5
View File
@@ -91,6 +91,7 @@ class UE(models.Model):
# All texts about the UE
title = models.CharField(_("title"), max_length=300)
manager = models.CharField(_("ue manager"), max_length=300)
objectives = models.TextField(_("objectives"))
program = models.TextField(_("program"))
skills = models.TextField(_("skills"))
@@ -125,11 +126,7 @@ class UE(models.Model):
Returns:
True if the user has already posted a comment on this UE, else False.
"""
self._has_user_commented = getattr(self, "_has_user_commented", {})
self._has_user_commented[user] = self._has_user_commented.get(
user, self.comments.filter(author=user).exists()
)
return self._has_user_commented[user]
return self.comments.filter(author=user).exists()
@cached_property
def grade_global_average(self):
+7 -12
View File
@@ -69,7 +69,6 @@ $pedagogy-white-text: #f0f0f0;
input[type="checkbox"] {
display: none;
}
margin-top: 10px;
margin-bottom: 0;
color: white;
@@ -129,14 +128,14 @@ $pedagogy-white-text: #f0f0f0;
grid-area: hours-the;
}
.leave-comment-not-allowed {
#leave_comment_not_allowed {
p {
text-align: center;
color: red;
}
}
.leave-comment {
#leave_comment {
.leave-comment-grid-container {
display: grid;
grid-template-columns: 270px auto;
@@ -169,6 +168,10 @@ $pedagogy-white-text: #f0f0f0;
.input-stars {
margin-top: 20px;
}
input[type="submit"] {
float: right;
}
}
.ue-details-container {
@@ -330,9 +333,8 @@ $pedagogy-white-text: #f0f0f0;
padding-left: 10px;
}
.action {
.actions {
float: right;
margin-top: 0;
}
}
@@ -400,7 +402,6 @@ $pedagogy-white-text: #f0f0f0;
a {
color: $pedagogy-white-text;
cursor: pointer;
}
a:hover {
@@ -443,9 +444,3 @@ details.accordion>.accordion-content {
border-color: $pedagogy-orange;
border-right: none;
}
.buttons {
display: flex;
justify-content: flex-end;
align-items: center;
}
@@ -1,31 +0,0 @@
<form
class="report-form"
hx-post="{{ request.get_full_path() }}"
hx-target="this"
hx-swap="outerHTML"
hx-disable="button input[type='submit']"
hx-trigger="submit"
>
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.reason.errors }}
{{ form.reason }}
{# Hidden fields #}
{{ form.reporter }}
{{ form.comment }}
<div class="buttons">
<button
class="btn btn-red left"
hx-get="{{ ue_detail_url }}"
hx-target="closest form"
hx-swap="outerHTML"
>
{% trans %}Cancel{% endtrans %}
</button>
<input class="btn btn-green" type="submit" value="{% trans %}Report{% endtrans %}" />
</div>
</form>
@@ -1,71 +0,0 @@
<div class="leave-comment">
{% if form.is_creation %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{% endif %}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
{% endif %}
<form
hx-post="{{ action }}"
hx-target="closest .leave-comment"
hx-swap="outerHTML"
hx-disabled-elt="find input[type='submit']"
>
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<div class="buttons">
<input type="submit" value="{% trans %}Comment{% endtrans %}" />
</div>
</form>
{% if form.is_creation %}
</div>
</details>
{% endif %}
<br>
</div>
@@ -1,91 +0,0 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% from "core/macros.jinja" import user_profile_link %}
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
<br>
{% endif %}
<section>
{% for comment in comments %}
<div id="comment-{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#comment-{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<button
class="btn btn-orange action"
hx-get="{{ url('pedagogy:comment_update', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
<i class="fa fa-pencil"></i> {% trans %}Edit{% endtrans %}
</button>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<form class="action"
hx-post="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}"
hx-confirm='{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}'
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% csrf_token %}
<button class="btn btn-red action">
<i class="fa fa-trash-can"></i> {% trans %}Delete{% endtrans %}
</button>
</form>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a
hx-get="{{ url('pedagogy:comment_report', comment_id=comment.id) }}"
hx-swap="outerHTML"
hx-target="#comment-{{ comment.id }}"
>
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
</section>
@@ -1,10 +0,0 @@
{% if object.has_user_already_commented(user) %}
<div class="leave-comment-not-allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
<br>
{% endif %}
{% if not object.has_user_already_commented(user) and user.has_perm("pedagogy.add_uecomment") %}
{{ add_comment_form }}
{% endif %}
@@ -1,27 +0,0 @@
<div class="ue-details-container">
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
+154 -25
View File
@@ -1,21 +1,6 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% if is_fragment %}
<hx-partial id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
</hx-partial>
<hx-partial id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
</hx-partial>
<hx-partial id="comments" hx-swap="innerMorph">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
</hx-partial>
{% else %}
{% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_profile_link %}
{% from "pedagogy/macros.jinja" import display_star %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
@@ -26,7 +11,6 @@
{% endblock %}
{% block content %}
<div class="pedagogy">
<div id="ue_detail">
<button onclick='(function(){
@@ -71,21 +55,166 @@
<br>
<div id="ue-grade">
{% include "pedagogy/fragments/ue_details/grade.jinja" %}
<div class="ue-details-container">
<div class="grade">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(object.grade_global_average) }}</p>
<p>{{ display_star(object.grade_utility_average) }}</p>
<p>{{ display_star(object.grade_interest_average) }}</p>
<p>{{ display_star(object.grade_teaching_average) }}</p>
<p>{{ display_star(object.grade_work_load_average) }}</p>
</div>
<div class="ue-infos">
<p><b>{% trans %}Objectives{% endtrans %}</b></p>
<p>{{ object.objectives|markdown }}</p>
<p><b>{% trans %}Program{% endtrans %}</b></p>
<p>{{ object.program|markdown }}</p>
<p><b>{% trans %}Earned skills{% endtrans %}</b></p>
<p>{{ object.skills|markdown }}</p>
<p><b>{% trans %}Key concepts{% endtrans %}</b></p>
<p>{{ object.key_concepts|markdown }}</p>
<p><b>{% trans %}UE manager: {% endtrans %}</b>{{ object.manager }}</p>
</div>
</div>
<br>
{% if object.has_user_already_commented(user) %}
<div id="leave_comment_not_allowed">
<p>{% trans %}You already posted a comment on this UE. If you want to comment again, please modify or delete your previous comment.{% endtrans %}</p>
</div>
{% elif user.has_perm("pedagogy.add_uecomment") %}
<details class="accordion" id="leave_comment" {% if form.errors %}open{%endif%}>
<summary>{% trans %}Leave comment{% endtrans %}</summary>
<div class="accordion-content">
<form action="{{ url('pedagogy:ue_detail', ue_id=object.id) }}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="leave-comment-grid-container">
<div class="form-stars">
{{ form.non_field_errors() }}
{{ form.author.errors }}
{{ form.ue.errors }}
<div id="comment-form">
{% include "pedagogy/fragments/ue_details/form.jinja" %}
{{ form.author }}
{{ form.ue }}
<div class="input-stars">
<label for="{{ form.grade_global.id_for_label }}">{{ form.grade_global.label }} :</label>
{{ form.grade_global.errors }}
{{ form.grade_global }}
</div>
<div id="comments">
{% include "pedagogy/fragments/ue_details/comments.jinja" %}
<div class="input-stars">
<label for="{{ form.grade_utility.id_for_label }}">{{ form.grade_utility.label }} :</label>
{{ form.grade_utility.errors }}
{{ form.grade_utility }}
</div>
<div class="input-stars">
<label for="{{ form.grade_interest.id_for_label }}">{{ form.grade_interest.label }} :</label>
{{ form.grade_interest.errors }}
{{ form.grade_interest }}
</div>
<div class="input-stars">
<label for="{{ form.grade_teaching.id_for_label }}">{{ form.grade_teaching.label }} :</label>
{{ form.grade_teaching.errors }}
{{ form.grade_teaching }}
</div>
<div class="input-stars">
<label for="{{ form.grade_work_load.id_for_label }}">{{ form.grade_work_load.label }} :</label>
{{ form.grade_work_load.errors }}
{{ form.grade_work_load }}
</div>
</div>
<div class="form-comment">
<label for="{{ form.comment.id_for_label }}">{{ form.comment.label }} :</label>
{{ form.comment.errors }}
{{ form.comment }}
</div>
</div>
<p><input type="submit" value="{% trans %}Comment{% endtrans %}" /></p>
</form>
</div>
</details>
{% endif %}
<br>
{% if comments %}
<h2>{% trans %}Comments{% endtrans %}</h2>
{% for comment in comments %}
<div id="{{ comment.id }}" class="comment-container">
<div class="grade-block">
<div class="grade-type">
<p>{% trans %}Global grade{% endtrans %}</p>
<p>{% trans %}Utility{% endtrans %}</p>
<p>{% trans %}Interest{% endtrans %}</p>
<p>{% trans %}Teaching{% endtrans %}</p>
<p>{% trans %}Work load{% endtrans %}</p>
</div>
<div class="grade-stars">
<p>{{ display_star(comment.grade_global) }}</p>
<p>{{ display_star(comment.grade_utility) }}</p>
<p>{{ display_star(comment.grade_interest) }}</p>
<p>{{ display_star(comment.grade_teaching) }}</p>
<p>{{ display_star(comment.grade_work_load) }}</p>
</div>
<div class="grade-extension"></div>
</div>
<div class="comment">
<div class="anchor">
<a href="{{ url('pedagogy:ue_detail', ue_id=ue.id) }}#{{ comment.id }}"><i class="fa fa-paragraph"></i></a>
</div>
{{ comment.comment|markdown }}
</div>
<div class="info">
{% if comment.is_reported %}
<p class="status-reported">
{% trans %}This comment has been reported{% endtrans %}
</p>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.change_comment") %}
<p class="actions">
<a href="{{ url('pedagogy:comment_update', comment_id=comment.id) }}">
{% trans %}Edit{% endtrans %}
</a>
{% endif %}
{% if comment.author_id == user.id or user.has_perm("pedagogy.delete_comment") %}
<a href="{{ url('pedagogy:comment_delete', comment_id=comment.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</p>
{% endif %}
</div>
<div class="comment-end-bar">
<div class="report">
<p>
<a href="{{ url('pedagogy:comment_report', comment_id=comment.id) }}">
{% trans %}Report this comment{% endtrans %}
</a>
</p>
</div>
<div class="date"><p>{{ comment.publish_date.strftime('%d/%m/%Y') }}</p></div>
<div class="author"><p>{{ user_profile_link(comment.author) }}</p></div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock %}
{% endif %}
+2 -8
View File
@@ -24,13 +24,12 @@
from django.urls import path
from pedagogy.views import (
UECommentCreateView,
UECommentDeleteView,
UECommentReportCreateView,
UECommentUpdateView,
UECreateView,
UEDeleteView,
UEDetailView,
UEDetailFormView,
UEGuideView,
UEModerationFormView,
UEUpdateView,
@@ -39,12 +38,7 @@ from pedagogy.views import (
urlpatterns = [
# Urls displaying the actual application for visitors
path("", UEGuideView.as_view(), name="guide"),
path("ue/<int:ue_id>/", UEDetailView.as_view(), name="ue_detail"),
path(
"ue/<int:ue_id>/comment",
UECommentCreateView.as_view(),
name="comment_create",
),
path("ue/<int:ue_id>/", UEDetailFormView.as_view(), name="ue_detail"),
path(
"comment/<int:comment_id>/edit/",
UECommentUpdateView.as_view(),
+36 -73
View File
@@ -16,7 +16,7 @@
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
@@ -26,11 +26,9 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Exists, OuterRef
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils.functional import cached_property
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
FormView,
TemplateView,
UpdateView,
@@ -38,7 +36,7 @@ from django.views.generic import (
from core.auth.mixins import PermissionOrAuthorRequiredMixin
from core.models import Notification, User
from core.views.mixins import AllowFragment, FragmentMixin, UseFragmentsMixin
from core.views import DetailFormView
from pedagogy.forms import (
UECommentForm,
UECommentModerationForm,
@@ -48,56 +46,37 @@ from pedagogy.forms import (
from pedagogy.models import UE, UEComment, UECommentReport
class UECommentCreateView(PermissionRequiredMixin, FragmentMixin, CreateView):
model = UEComment
template_name = "pedagogy/fragments/ue_comment_form.jinja"
form_class = UECommentForm
permission_required = "pedagogy.add_uecomment"
object = None # Avoid initialisation bug with FragmentMixin
class UEDetailFormView(PermissionRequiredMixin, DetailFormView):
"""Display every comment of an UE and detailed infos about it.
@cached_property
def ue(self):
return get_object_or_404(UE, id=self.kwargs.get("ue_id"))
Allow to comment the UE.
"""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
form_class = UECommentForm
permission_required = "pedagogy.view_ue"
def has_permission(self):
if self.ue.has_user_already_commented(self.request.user):
if self.request.method == "POST" and not self.request.user.has_perm(
"pedagogy.add_uecomment"
):
# if it's a POST request, the user is trying to add a new UEComment
# thus he also needs the "add_uecomment" permission
return False
return super().has_permission()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["author_id"] = self.request.user.id
kwargs["ue_id"] = self.ue.id
kwargs["ue_id"] = self.object.id
kwargs["is_creation"] = True
return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse("pedagogy:comment_create", kwargs={"ue_id": self.ue.id}),
"object": self.ue,
}
def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue.id})
class UEDetailView(
PermissionRequiredMixin, UseFragmentsMixin, AllowFragment, DetailView
):
"""Display every comment of an UE and detailed infos about it."""
model = UE
pk_url_kwarg = "ue_id"
template_name = "pedagogy/ue_detail.jinja"
permission_required = "pedagogy.view_ue"
fragments = {
"add_comment_form": UECommentCreateView,
}
def get_fragment_data(self):
return {
"add_comment_form": {"ue_id": self.object.id},
}
def form_valid(self, form):
form.save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
@@ -106,17 +85,22 @@ class UEDetailView(
.annotate_is_reported()
.select_related("author")
.order_by("-publish_date")
),
)
}
def get_success_url(self):
# once the new ue comment has been saved
# redirect to the same page we are currently
return self.request.path
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, UpdateView):
class UECommentUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
"""Allow edit of a given comment."""
model = UEComment
form_class = UECommentForm
pk_url_kwarg = "comment_id"
template_name = "pedagogy/fragments/ue_comment_form.jinja"
template_name = "core/edit.jinja"
permission_required = "pedagogy.change_uecomment"
author_field = "author"
@@ -127,18 +111,11 @@ class UECommentUpdateView(PermissionOrAuthorRequiredMixin, AllowFragment, Update
kwargs["is_creation"] = False
return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"action": reverse(
"pedagogy:comment_update", kwargs={"comment_id": self.object.id}
)
}
def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue.id})
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, DeleteView):
class UECommentDeleteView(PermissionOrAuthorRequiredMixin, DeleteView):
"""Allow to delete a given comment."""
model = UEComment
@@ -147,13 +124,8 @@ class UECommentDeleteView(PermissionOrAuthorRequiredMixin, AllowFragment, Delete
permission_required = "pedagogy.delete_uecomment"
author_field = "author"
def form_valid(self, form):
response = super().form_valid(form)
response.headers["HX-Trigger"] = "CommentUpdate"
return response
def get_success_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue.id})
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.object.ue_id})
class UEGuideView(PermissionRequiredMixin, TemplateView):
@@ -163,34 +135,24 @@ class UEGuideView(PermissionRequiredMixin, TemplateView):
permission_required = "pedagogy.view_ue"
class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateView):
class UECommentReportCreateView(PermissionRequiredMixin, CreateView):
"""Create a new report for an inappropriate comment."""
model = UECommentReport
form_class = UECommentReportForm
template_name = "pedagogy/fragments/comment_report.jinja"
template_name = "core/edit.jinja"
permission_required = "pedagogy.add_uecommentreport"
def dispatch(self, request, *args, **kwargs):
self.ue_comment = get_object_or_404(UEComment, pk=kwargs["comment_id"])
return super().dispatch(request, *args, **kwargs)
@cached_property
def ue_detail_url(self):
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue.id})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["reporter_id"] = self.request.user.id
kwargs["comment_id"] = self.ue_comment.id
return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data() | {
"comment_id": self.ue_comment.id,
"ue_detail_url": self.ue_detail_url,
}
def form_valid(self, form):
resp = super().form_valid(form)
# Send a message to moderation admins
@@ -206,10 +168,11 @@ class UECommentReportCreateView(PermissionRequiredMixin, AllowFragment, CreateVi
url=reverse("pedagogy:moderation"),
type="PEDAGOGY_MODERATION",
)
return resp
def get_success_url(self):
return self.ue_detail_url
return reverse("pedagogy:ue_detail", kwargs={"ue_id": self.ue_comment.ue_id})
class UEModerationFormView(PermissionRequiredMixin, FormView):
+17 -24
View File
@@ -17,25 +17,25 @@ authors = [
{ name = "Vial", email = "robin.trioux@utbm.fr" },
]
license = { text = "GPL-3.0-only" }
requires-python = "<4.0,>=3.14"
requires-python = "<4.0,>=3.12"
dependencies = [
"django>=5.2.17,<6.0.0",
"django-ninja>=1.7.0,<2.0.0",
"django-ninja>=1.6.3,<2.0.0",
"django-ninja-extra>=0.31.7",
"Pillow>=12.3.0,<13.0.0",
"aemark>=0.1.1",
"django-jinja<3.0.0,>=2.11.0",
"cryptography>=50.0.1,<51.0.0",
"cryptography>=50.0.0,<51.0.0",
"django-phonenumber-field>=8.5.0,<9.0.0",
"phonenumbers>=9.0.38,<10.0.0",
"phonenumbers>=9.0.37,<10.0.0",
"reportlab>=5.0.1,<6.0.0",
"django-haystack>=3.4.0,<4.0.0",
"xapian-haystack>=4.0.0,<5.0.0",
"libsass>=0.23.0,<1.0.0",
"django-ordered-model>=3.7.4,<4.0.0",
"django-simple-captcha>=0.7.0,<1.0.0",
"python-dateutil>=2.9.0.post0,<3.0.0",
"sentry-sdk>=2.68.1,<3.0.0",
"python-dateutil>=2.9.0.post0,<3.0.0.0",
"sentry-sdk>=2.68.0,<3.0.0",
"jinja2>=3.1.6,<4.0.0",
"django-countries>=9.0.0,<10.0.0",
"dict2xml>=1.7.8,<2.0.0",
@@ -43,13 +43,13 @@ dependencies = [
"tomli>=2.4.1,<3.0.0",
"django-honeypot>=1.3.0,<2",
"pydantic-extra-types>=2.11.1,<3.0.0",
"ical>=14.1.1",
"ical>=12.0.0",
"redis[hiredis]>=6.4.0,<9.0.0",
"environs[django]>=15.1.0,<16",
"requests>=2.34.2,<3.0.0",
"honcho>=2.0.0",
"psutil>=7.2.2,<8.0.0",
"celery[redis]>=5.6.2,<7",
"celery[redis]>=5.6.3,<8",
"django-celery-results>=2.6.0",
"django-celery-beat>=2.9.0",
]
@@ -63,12 +63,12 @@ prod = [
"psycopg[c]>=3.3.4,<4.0.0",
]
dev = [
"django-debug-toolbar>=8.0.0,<9",
"ipython>=9.17.1,<10.0.0",
"pre-commit>=4.6.1,<5.0.0",
"ruff>=0.16.6,<1.0.0",
"django-debug-toolbar>=7.1.1,<8",
"ipython>=9.16.1,<10.0.0",
"pre-commit>=4.6.0,<5.0.0",
"ruff>=0.16.0,<1.0.0",
"djhtml>=3.0.11,<4.0.0",
"faker>=40.38.0,<41.0.0",
"faker>=40.37.0,<41.0.0",
"rjsmin>=1.2.5,<2.0.0",
]
tests = [
@@ -78,7 +78,7 @@ tests = [
"pytest-django>=4.14.0,<5.0.0",
"model-bakery>=1.24.0,<2.0.0",
"beautifulsoup4>=4.15.0,<5",
"lxml>=6.1.3,<7",
"lxml>=6.1.2,<7",
]
docs = [
"mkdocs>=1.6.1,<2.0.0",
@@ -92,11 +92,11 @@ docs = [
default-groups = ["dev", "tests", "docs"]
[tool.xapian]
version = "2.1.0"
version = "2.0.0"
# Those hashes are here to protect against supply chains attacks
# See `https://ae-utbm.github.io/sith/howto/xapian/` for more information
core-sha256 = "8e1259586d342e3d12b5e1f772e9185a10f2ba16e541566b5c3c239f71b8aacc"
bindings-sha256 = "f52ec189f13b4fa66ea625a6eb94bb32dd651b9ec806be6a911dda54cbe3875c"
core-sha256 = "6cea3f49952a47224439a40bdb3608f928d121ad8721b9921cc42802d548ecf8"
bindings-sha256 = "9a544b69c31355a92edbcd4102cf0f1ec4407fd0a4645f4870fb52300b736910"
[tool.ruff]
output-format = "concise" # makes ruff error logs easier to read
@@ -132,13 +132,6 @@ ignore = [
"RUF012" # mutable class attributes. This rule doesn't integrate well with django
]
[tool.ruff.lint.per-file-ignores]
# schemas and API routes are built upon ninja and pydantic,
# which heavily rely on type annotations at runtime.
# Systematically using TYPE_CHECKING blocks in those files is NOT safe
"schemas.py" = ["TCH"]
"api.py" = ["TCH"]
[tool.ruff.lint.pydocstyle]
convention = "google"
+1 -4
View File
@@ -1,8 +1,8 @@
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
import pytest
from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse
from django.utils.timezone import localtime
from model_bakery import baker
@@ -10,9 +10,6 @@ from pytest_django.asserts import assertRedirects
from core.models import BanGroup, User, UserBan
if TYPE_CHECKING:
from django.test import Client
@pytest.fixture
def operator(db) -> User:
+1 -2
View File
@@ -9,6 +9,7 @@ from django.utils.timezone import get_current_timezone
from django.utils.translation import gettext_lazy as _
from PIL import Image
from core.models import User
from core.utils import resize_image
from core.views import MultipleImageField
from core.views.forms import SelectDate
@@ -19,8 +20,6 @@ from sas.widgets.ajax_select import AutoCompleteSelectAlbum
if TYPE_CHECKING:
from django.db.models.fields.files import FieldFile
from core.models import User
class AlbumCreateForm(forms.ModelForm):
class Meta:
+2 -4
View File
@@ -1,13 +1,14 @@
import random
import string
from pathlib import Path
from typing import TYPE_CHECKING, Callable
from typing import Callable
from unittest.mock import patch
import pytest
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client
from django.urls import reverse
from django.utils.datastructures import MultiValueDict
from django.utils.timezone import localdate
@@ -22,9 +23,6 @@ from sas.baker_recipes import picture_recipe
from sas.forms import AlbumEditForm
from sas.models import Album
if TYPE_CHECKING:
from django.test import Client
@pytest.fixture
def sas_root(db) -> Album:
+2 -4
View File
@@ -12,7 +12,7 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from typing import TYPE_CHECKING, Any
from typing import Any
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -21,6 +21,7 @@ from django.db.models import Count, OuterRef, Subquery
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.safestring import SafeString
from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView, UpdateView
@@ -39,9 +40,6 @@ from sas.forms import (
)
from sas.models import Album, PeoplePictureRelation, Picture
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class AlbumCreateFragment(FragmentMixin, CreateView):
model = Album
+1 -4
View File
@@ -2,15 +2,12 @@ import logging
import signal
import subprocess
import sys
from typing import TYPE_CHECKING
from pathlib import Path
import psutil
from sith import settings
if TYPE_CHECKING:
from pathlib import Path
def get_pid_file(procfile: Path) -> Path:
"""Get the PID file associated with a procfile"""
+2 -5
View File
@@ -1,13 +1,10 @@
from contextlib import nullcontext as does_not_raise
from typing import TYPE_CHECKING
import pytest
from django.test.utils import override_settings
from django.urls import reverse
if TYPE_CHECKING:
from _pytest.raises import RaisesExc
from django.test import Client
from django.test.utils import override_settings
from django.urls import reverse
@pytest.mark.django_db
+1 -4
View File
@@ -1,17 +1,14 @@
from pathlib import Path
from typing import TYPE_CHECKING
from django.conf import settings
from django.contrib.staticfiles.finders import find
from django.contrib.staticfiles.storage import (
ManifestStaticFilesStorage,
)
from django.core.files.storage import Storage
from staticfiles.processors import JS, JSBundler, Scss
if TYPE_CHECKING:
from django.core.files.storage import Storage
class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
def url(self, name: str, *, force: bool = False) -> str:
@@ -1,10 +1,6 @@
# Generated by Django 5.2.3 on 2025-09-08 05:38
from typing import TYPE_CHECKING
from django.db import migrations, models
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
@@ -1,16 +1,12 @@
# Generated by Django 5.2.17 on 2026-09-01 08:44
from typing import TYPE_CHECKING
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F, Value
from django.db.models.functions import Least
from django.utils.timezone import now
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def make_default_creation_date(apps: StateApps, schema_editor):
Subscription = apps.get_model("subscription", "Subscription")
+2 -4
View File
@@ -1,12 +1,13 @@
"""Tests focused on testing subscription creation"""
from datetime import date, timedelta
from typing import TYPE_CHECKING, Callable
from typing import Callable
import pytest
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse
from django.utils.timezone import localdate
from model_bakery import baker
@@ -18,9 +19,6 @@ from counter.models import Customer
from subscription.forms import SubscriptionExistingUserForm, SubscriptionNewUserForm
from subscription.models import Subscription
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db
@pytest.mark.parametrize(
Generated
+642 -296
View File
File diff suppressed because it is too large Load Diff