Compare commits

..
Author SHA1 Message Date
dependabot[bot] 1d89770f31 [UPDATE] Update mkdocstrings-python requirement
Updates the requirements on [mkdocstrings-python](https://github.com/mkdocstrings/python) to permit the latest version.
- [Release notes](https://github.com/mkdocstrings/python/releases)
- [Changelog](https://github.com/mkdocstrings/python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mkdocstrings/python/compare/2.0.4...2.0.5)

---
updated-dependencies:
- dependency-name: mkdocstrings-python
  dependency-version: 2.0.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 08:23:04 +00:00
152 changed files with 2168 additions and 2815 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.16.0 rev: v0.15.19
hooks: hooks:
- id: ruff-check # just check the code, and print the errors - id: ruff-check # just check the code, and print the errors
- id: ruff-check # actually fix the fixable errors, but print nothing - id: ruff-check # actually fix the fixable errors, but print nothing
+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.contrib import admin, messages
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from api.hashers import generate_key from api.hashers import generate_key
from api.models import ApiClient, ApiKey from api.models import ApiClient, ApiKey
if TYPE_CHECKING:
from django.db.models import QuerySet
from django.http import HttpRequest
@admin.register(ApiClient) @admin.register(ApiClient)
class ApiClientAdmin(admin.ModelAdmin): 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 ninja.security import APIKeyHeader
from api.hashers import get_hasher from api.hashers import get_hasher
from api.models import ApiClient, ApiKey from api.models import ApiClient, ApiKey
if TYPE_CHECKING:
from django.http import HttpRequest
class ApiKeyAuth(APIKeyHeader): class ApiKeyAuth(APIKeyHeader):
"""Authentication through client api keys.""" """Authentication through client api keys."""
+4 -6
View File
@@ -39,17 +39,15 @@ Example:
import operator import operator
from functools import reduce from functools import reduce
from typing import TYPE_CHECKING, Any, Callable from typing import Any, Callable
from django.contrib.auth.models import Permission
from django.http import HttpRequest
from ninja_extra import ControllerBase
from ninja_extra.permissions import BasePermission from ninja_extra.permissions import BasePermission
from counter.utils import is_logged_in_counter 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
class IsInGroup(BasePermission): class IsInGroup(BasePermission):
"""Check that the user is in the group whose primary key is given.""" """Check that the user is in the group whose primary key is given."""
+2 -6
View File
@@ -12,16 +12,12 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from typing import TYPE_CHECKING
from django.contrib import admin from django.contrib import admin
from django.forms.models import ModelForm
from django.http import HttpRequest
from club.models import Club, ClubLink, ClubRole, LinkType, Membership from club.models import Club, ClubLink, ClubRole, LinkType, Membership
if TYPE_CHECKING:
from django.forms.models import ModelForm
from django.http import HttpRequest
@admin.register(Club) @admin.register(Club)
class ClubAdmin(admin.ModelAdmin): class ClubAdmin(admin.ModelAdmin):
@@ -1,16 +1,12 @@
# Generated by Django 4.2.16 on 2024-11-20 17:08 # 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.deletion
import django.db.models.functions.datetime import django.db.models.functions.datetime
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Q from django.db.models import Q
from django.utils.timezone import localdate 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 # Before the club role rework, the maximum free role
# was the hardcoded highest non-board role # was the hardcoded highest non-board role
MAXIMUM_FREE_ROLE = 1 MAXIMUM_FREE_ROLE = 1
@@ -1,14 +1,10 @@
# Generated by Django 5.2.3 on 2025-06-21 21:59 # Generated by Django 5.2.3 on 2025-06-21 21:59
from typing import TYPE_CHECKING
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Case, When from django.db.models import Case, When
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
PRESIDENCY_ROLES = [10, 9] PRESIDENCY_ROLES = [10, 9]
MAXIMUM_FREE_ROLE = 1 MAXIMUM_FREE_ROLE = 1
SITH_CLUB_ROLES = { SITH_CLUB_ROLES = {
@@ -1,6 +1,7 @@
import type { escape_html } from "tom-select/src/utils"; import type { TomOption } from "tom-select/dist/types/types";
import { AjaxSelect } from "#core:core/components/ajax-select-base"; import type { escape_html } from "tom-select/dist/types/utils";
import { registerComponent } from "#core:utils/web-components"; import { AjaxSelect } from "#core:core/components/ajax-select-base.ts";
import { registerComponent } from "#core:utils/web-components.ts";
import { type ClubSchema, clubSearchClub } from "#openapi"; import { type ClubSchema, clubSearchClub } from "#openapi";
@registerComponent("club-ajax-select") @registerComponent("club-ajax-select")
@@ -9,7 +10,7 @@ export class ClubAjaxSelect extends AjaxSelect {
protected labelField = "name"; protected labelField = "name";
protected searchField = ["code", "name"]; protected searchField = ["code", "name"];
protected async search(query: string) { protected async search(query: string): Promise<TomOption[]> {
const resp = await clubSearchClub({ query: { search: query } }); const resp = await clubSearchClub({ query: { search: query } });
if (resp.data) { if (resp.data) {
return resp.data.results; return resp.data.results;
+6 -11
View File
@@ -15,6 +15,9 @@
{% endblock %} {% endblock %}
{% else %} {% else %}
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endblock %}
{% block description -%} {% block description -%}
{% trans %}The list of all clubs existing at UTBM.{% endtrans %} {% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %} {%- endblock %}
@@ -23,12 +26,6 @@
{%- endblock %} {%- endblock %}
{% endif %} {% endif %}
{% block additional_css %}
{% if not is_fragment %}
<link rel="stylesheet" href="{{ static("club/list.scss") }}">
{% endif %}
{% endblock %}
{% from "core/macros.jinja" import paginate_htmx %} {% from "core/macros.jinja" import paginate_htmx %}
{% block content %} {% block content %}
@@ -36,17 +33,15 @@
<h3>{% trans %}Filters{% endtrans %}</h3> <h3>{% trans %}Filters{% endtrans %}</h3>
<form <form
id="club-list-filters" id="club-list-filters"
method="GET" hx-get="{{ url("club:club_list") }}"
hx-action="{{ url("club:club_list") }}"
hx-target="#content" hx-target="#content"
hx-swap="innerHTML" hx-swap="outerHtml"
hx-push-url="true" hx-push-url="true"
hx-disable="find input, find button"
> >
<div class="row gap-4x"> <div class="row gap-4x">
{{ form }} {{ form }}
</div> </div>
<button class="btn btn-blue margin-bottom"> <button type="submit" class="btn btn-blue margin-bottom">
<i class="fa fa-magnifying-glass"></i>{% trans %}Search{% endtrans %} <i class="fa fa-magnifying-glass"></i>{% trans %}Search{% endtrans %}
</button> </button>
</form> </form>
@@ -7,7 +7,7 @@
<form <form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}" hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disable="find input[type='submit']" hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#member-fragment-container" hx-target="#member-fragment-container"
id="add_club_members_form" id="add_club_members_form"
+1 -4
View File
@@ -1,9 +1,9 @@
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.db import ProgrammingError from django.db import ProgrammingError
from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import localdate from django.utils.timezone import localdate
from model_bakery import baker 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.baker_recipes import subscriber_user
from core.models import User from core.models import User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_club_queryset_having_board_member(): 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 import pytest
from django.contrib.auth.models import Permission 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.baker_recipes import subscriber_user
from core.models import AnonymousUser, User from core.models import AnonymousUser, User
if TYPE_CHECKING:
from collections.abc import Callable
def make_club(): def make_club():
# unittest-style tests cannot use fixture, so we create a function # 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 import pytest
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertRedirects from pytest_django.asserts import assertRedirects
@@ -8,9 +7,6 @@ from pytest_django.asserts import assertRedirects
from club.models import Club, ClubRole, Membership from club.models import Club, ClubRole, Membership
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_club_board_member_cannot_edit_club_properties(client: Client): def test_club_board_member_cannot_edit_club_properties(client: Client):
+1 -4
View File
@@ -1,6 +1,6 @@
import itertools import itertools
from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
import pytest import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -19,9 +19,6 @@ from club.tests.base import TestClub
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User from core.models import AnonymousUser, User
if TYPE_CHECKING:
from collections.abc import Callable
class TestMembershipQuerySet(TestClub): class TestMembershipQuerySet(TestClub):
def test_ongoing(self): def test_ongoing(self):
+1 -5
View File
@@ -1,8 +1,7 @@
from typing import TYPE_CHECKING
import pytest import pytest
from aemark import markdown from aemark import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertHTMLEqual, assertRedirects 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.baker_recipes import subscriber_user
from core.models import PageRev, User from core.models import PageRev, User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_page_display_on_club_main_page(client: Client): 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 import pytest
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
@@ -8,9 +7,6 @@ from club.models import Club
from com.models import Poster from com.models import Poster
from core.baker_recipes import subscriber_user from core.baker_recipes import subscriber_user
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize("route_url", ["club:poster_list", "club:poster_create"]) @pytest.mark.parametrize("route_url", ["club:poster_list", "club:poster_create"])
+1 -4
View File
@@ -1,8 +1,8 @@
import csv import csv
import itertools import itertools
from typing import TYPE_CHECKING
import pytest import pytest
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker 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.baker_recipes import product_recipe, sale_recipe
from counter.models import Counter, Customer, Product, Selling from counter.models import Counter, Customer, Product, Selling
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_sales_page_doesnt_crash(client: Client): def test_sales_page_doesnt_crash(client: Client):
+1 -4
View File
@@ -1,5 +1,4 @@
from datetime import date from datetime import date
from typing import TYPE_CHECKING
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django import forms from django import forms
@@ -10,13 +9,11 @@ from django.utils.translation import gettext_lazy as _
from club.models import Club from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub from club.widgets.ajax_select import AutoCompleteSelectClub
from com.models import News, NewsDate, Poster from com.models import News, NewsDate, Poster
from core.models import User
from core.utils import get_end_of_semester from core.utils import get_end_of_semester
from core.views.forms import SelectDateTime from core.views.forms import SelectDateTime
from core.views.widgets.markdown import MarkdownInput from core.views.widgets.markdown import MarkdownInput
if TYPE_CHECKING:
from core.models import User
class PosterForm(forms.ModelForm): class PosterForm(forms.ModelForm):
class Meta: class Meta:
+3 -8
View File
@@ -1,10 +1,11 @@
from typing import TYPE_CHECKING from pathlib import Path
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.contrib.syndication.views import add_domain from django.contrib.syndication.views import add_domain
from django.db.models import Count, OuterRef, QuerySet, Subquery from django.db.models import Count, OuterRef, QuerySet, Subquery
from django.http import HttpRequest
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from ical.calendar import Calendar from ical.calendar import Calendar
@@ -13,13 +14,7 @@ from ical.event import Event
from ical.types import Frequency, Recur from ical.types import Frequency, Recur
from com.models import News, NewsDate from com.models import News, NewsDate
from core.models import User
if TYPE_CHECKING:
from pathlib import Path
from django.http import HttpRequest
from core.models import User
def as_absolute_url(url: str, request: HttpRequest | None = None) -> str: def as_absolute_url(url: str, request: HttpRequest | None = None) -> str:
@@ -1,9 +1,4 @@
import { import { Calendar, type EventClickArg, type EventContentArg } from "@fullcalendar/core";
Calendar,
type EventClickArg,
type EventContentArg,
type EventInput,
} from "@fullcalendar/core";
import type { EventImpl } from "@fullcalendar/core/internal"; import type { EventImpl } from "@fullcalendar/core/internal";
import enLocale from "@fullcalendar/core/locales/en-gb"; import enLocale from "@fullcalendar/core/locales/en-gb";
import frLocale from "@fullcalendar/core/locales/fr"; import frLocale from "@fullcalendar/core/locales/fr";
@@ -11,8 +6,8 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import iCalendarPlugin from "@fullcalendar/icalendar"; import iCalendarPlugin from "@fullcalendar/icalendar";
import listPlugin from "@fullcalendar/list"; import listPlugin from "@fullcalendar/list";
import { type HTMLTemplateResult, html, render } from "lit-html"; import { type HTMLTemplateResult, html, render } from "lit-html";
import { type GenericEndpoint, makeUrl } from "#core:utils/api"; import { makeUrl } from "#core:utils/api.ts";
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts";
import { import {
calendarCalendarInternal, calendarCalendarInternal,
calendarCalendarUnpublished, calendarCalendarUnpublished,
@@ -24,11 +19,11 @@ import {
@registerComponent("ics-calendar") @registerComponent("ics-calendar")
export class IcsCalendar extends inheritHtmlElement("div") { export class IcsCalendar extends inheritHtmlElement("div") {
static observedAttributes = ["locale", "can_moderate", "can_delete", "ics-help-url"]; static observedAttributes = ["locale", "can_moderate", "can_delete", "ics-help-url"];
private calendar!: Calendar; private calendar: Calendar;
private locale? = "en"; private locale = "en";
private canModerate? = false; private canModerate = false;
private canDelete? = false; private canDelete = false;
private helpUrl? = ""; private helpUrl = "";
// Hack variable to detect recurring events // Hack variable to detect recurring events
// The underlying ics library doesn't include any info about rrules // The underlying ics library doesn't include any info about rrules
@@ -40,12 +35,10 @@ export class IcsCalendar extends inheritHtmlElement("div") {
this.locale = newValue; this.locale = newValue;
} }
if (name === "can_moderate") { if (name === "can_moderate") {
this.canModerate = this.canModerate = newValue.toLowerCase() === "true";
typeof newValue === "string" && newValue.toLowerCase() === "true";
} }
if (name === "can_delete") { if (name === "can_delete") {
this.canDelete = this.canDelete = newValue.toLowerCase() === "true";
typeof newValue === "string" && newValue.toLowerCase() === "true";
} }
if (name === "ics-help-url") { if (name === "ics-help-url") {
@@ -101,7 +94,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
.toString() .toString()
.split("/") .split("/")
.filter((s) => s) // Remove blank characters .filter((s) => s) // Remove blank characters
.pop() as string, .pop(),
10, 10,
); );
} }
@@ -166,7 +159,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
this.refreshEvents(); this.refreshEvents();
} }
async getEventSources(): Promise<EventInput[]> { async getEventSources() {
const tagRecurringEvents = (eventData: EventImpl) => { const tagRecurringEvents = (eventData: EventImpl) => {
// This functions tags events with a similar event url // This functions tags events with a similar event url
// We rely on the fact that the event url is always the same // We rely on the fact that the event url is always the same
@@ -180,14 +173,14 @@ export class IcsCalendar extends inheritHtmlElement("div") {
}; };
return [ return [
{ {
url: `${await makeUrl(calendarCalendarInternal as GenericEndpoint)}`, url: `${await makeUrl(calendarCalendarInternal)}`,
format: "ics", format: "ics",
className: "internal", className: "internal",
cache: false, cache: false,
eventDataTransform: tagRecurringEvents, eventDataTransform: tagRecurringEvents,
}, },
{ {
url: `${await makeUrl(calendarCalendarUnpublished as GenericEndpoint)}`, url: `${await makeUrl(calendarCalendarUnpublished)}`,
format: "ics", format: "ics",
color: "red", color: "red",
className: "unpublished", className: "unpublished",
@@ -220,7 +213,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
${event.title} ${event.title}
</h4> </h4>
<span class="event-details-row-content"> <span class="event-details-row-content">
${this.formatDate(event.start as Date)} - ${this.formatDate(event.end as Date)} ${this.formatDate(event.start)} - ${this.formatDate(event.end)}
</span> </span>
</div> </div>
`; `;
@@ -258,7 +251,7 @@ export class IcsCalendar extends inheritHtmlElement("div") {
const buttons = [] as HTMLTemplateResult[]; const buttons = [] as HTMLTemplateResult[];
if (this.canModerate) { if (this.canModerate) {
if (event.source?.internalEventSource.ui.classNames.includes("unpublished")) { if (event.source.internalEventSource.ui.classNames.includes("unpublished")) {
const button = html` const button = html`
<button class="btn btn-green" @click="${() => this.publishNews(newsId)}"> <button class="btn btn-green" @click="${() => this.publishNews(newsId)}">
<i class="fa fa-check"></i>${gettext("Publish")} <i class="fa fa-check"></i>${gettext("Publish")}
@@ -345,9 +338,9 @@ export class IcsCalendar extends inheritHtmlElement("div") {
button.classList.remove("text-copied"); button.classList.remove("text-copied");
} }
button.setAttribute("tooltip", gettext("Link copied")); button.setAttribute("tooltip", gettext("Link copied"));
await navigator.clipboard.writeText( navigator.clipboard.writeText(
new URL( new URL(
await makeUrl(calendarCalendarInternal as GenericEndpoint), await makeUrl(calendarCalendarInternal),
window.location.origin, window.location.origin,
).toString(), ).toString(),
); );
@@ -1,9 +1,4 @@
import { import { newsDeleteNews, newsFetchNewsDates, newsPublishNews } from "#openapi";
newsDeleteNews,
newsFetchNewsDates,
newsPublishNews,
type PaginatedResponseSchemaNewsDateSchema,
} from "#openapi";
// This will be used in jinja templates, // This will be used in jinja templates,
// so we cannot use real enums as those are purely an abstraction of Typescript // so we cannot use real enums as those are purely an abstraction of Typescript
@@ -69,8 +64,7 @@ document.addEventListener("alpine:init", () => {
// biome-ignore lint/style/useNamingConvention: api is snake-case // biome-ignore lint/style/useNamingConvention: api is snake-case
query: { news_id: this.newsId, page: 1, page_size: 1 }, query: { news_id: this.newsId, page: 1, page_size: 1 },
}); });
return response.data.count;
return (response.data as PaginatedResponseSchemaNewsDateSchema).count;
}, },
weeklyEventWarningMessage(nbEvents: number): string { weeklyEventWarningMessage(nbEvents: number): string {
@@ -1,11 +1,11 @@
import { type NewsDateSchema, newsFetchNewsDates } from "#openapi"; import { type NewsDateSchema, newsFetchNewsDates } from "#openapi";
type ParsedNewsDateSchema = Omit<NewsDateSchema, "start_date" | "end_date"> & { interface ParsedNewsDateSchema extends Omit<NewsDateSchema, "start_date" | "end_date"> {
// biome-ignore lint/style/useNamingConvention: api is snake_case // biome-ignore lint/style/useNamingConvention: api is snake_case
start_date: Date; start_date: Date;
// biome-ignore lint/style/useNamingConvention: api is snake_case // biome-ignore lint/style/useNamingConvention: api is snake_case
end_date: Date; end_date: Date;
}; }
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("upcomingNewsLoader", (startDate: Date, locale: string) => ({ Alpine.data("upcomingNewsLoader", (startDate: Date, locale: string) => ({
@@ -32,11 +32,6 @@ document.addEventListener("alpine:init", () => {
page_size: this.pageSize, page_size: this.pageSize,
}, },
}); });
if (response.response === undefined || response.data === undefined) {
// response may be undefined, because error may be
// from building the request object itself or from a network error
return;
}
if (response.response.status === 404) { if (response.response.status === 404) {
this.hasNext = false; this.hasNext = false;
} else if (response.data.next === null) { } else if (response.data.next === null) {
@@ -49,7 +44,7 @@ document.addEventListener("alpine:init", () => {
this.loading = false; this.loading = false;
}, },
groupedDates(): Record<string, ParsedNewsDateSchema[]> { groupedDates(): Record<string, NewsDateSchema[]> {
return this.newsDates return this.newsDates
.map( .map(
(date: NewsDateSchema): ParsedNewsDateSchema => ({ (date: NewsDateSchema): ParsedNewsDateSchema => ({
+1 -4
View File
@@ -1,11 +1,11 @@
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
import pytest import pytest
from aemark import markdown from aemark import markdown
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.http import HttpResponse
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
@@ -16,9 +16,6 @@ from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate from com.models import News, NewsDate
from core.models import User from core.models import User
if TYPE_CHECKING:
from django.http import HttpResponse
def accel_redirect_to_file(response: HttpResponse) -> Path | None: def accel_redirect_to_file(response: HttpResponse) -> Path | None:
redirect = Path(response.headers.get("X-Accel-Redirect", "")) redirect = Path(response.headers.get("X-Accel-Redirect", ""))
+13 -3
View File
@@ -1,8 +1,9 @@
from typing import Annotated, Literal from typing import Annotated, Any, Literal
from annotated_types import Ge, Le, MinLen from annotated_types import Ge, Le, MinLen
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from django.http import HttpResponse
from ninja import File, Query from ninja import File, Query
from ninja.security import SessionAuth from ninja.security import SessionAuth
from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra import ControllerBase, api_controller, paginate, route
@@ -17,6 +18,7 @@ from core.models import Group, QuickUploadImage, SithFile, User
from core.schemas import ( from core.schemas import (
FamilyGodfatherSchema, FamilyGodfatherSchema,
GroupSchema, GroupSchema,
MarkdownSchema,
SithFileSchema, SithFileSchema,
UploadedFileSchema, UploadedFileSchema,
UploadedImage, UploadedImage,
@@ -24,18 +26,26 @@ from core.schemas import (
UserFilterSchema, UserFilterSchema,
UserProfileSchema, UserProfileSchema,
UserSchema, UserSchema,
ValidationErrorSchema,
) )
from core.templatetags.renderer import markdown
from counter.utils import is_logged_in_counter from counter.utils import is_logged_in_counter
@api_controller("/markdown")
class MarkdownController(ControllerBase):
@route.post("", url_name="markdown")
def render_markdown(self, body: MarkdownSchema):
"""Convert the markdown text into html."""
return HttpResponse(markdown(body.text), content_type="text/html")
@api_controller("/upload") @api_controller("/upload")
class UploadController(ControllerBase): class UploadController(ControllerBase):
@route.post( @route.post(
"/image", "/image",
response={ response={
200: UploadedFileSchema, 200: UploadedFileSchema,
422: ValidationErrorSchema, 422: dict[Literal["detail"], list[dict[str, Any]]],
403: dict[Literal["detail"], str], 403: dict[Literal["detail"], str],
}, },
permissions=[HasPerm("core.add_quickuploadimage")], permissions=[HasPerm("core.add_quickuploadimage")],
@@ -1,13 +1,9 @@
# Generated by Django 4.2.17 on 2025-01-04 16:42 # 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 import migrations
from django.db.migrations.state import StateApps
from django.db.models import F from django.db.models import F
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def invert_is_manually_manageable(apps: StateApps, schema_editor): def invert_is_manually_manageable(apps: StateApps, schema_editor):
"""Invert `is_manually_manageable`. """Invert `is_manually_manageable`.
@@ -1,14 +1,10 @@
# Generated by Django 4.2.17 on 2024-12-31 13:30 # Generated by Django 4.2.17 on 2024-12-31 13:30
from typing import TYPE_CHECKING
import django.contrib.auth.models import django.contrib.auth.models
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def migrate_ban_groups(apps: StateApps, schema_editor): def migrate_ban_groups(apps: StateApps, schema_editor):
@@ -1,15 +1,11 @@
# Generated by Django 5.2.12 on 2026-05-01 08:59 # Generated by Django 5.2.12 on 2026-05-01 08:59
from typing import TYPE_CHECKING
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import F from django.db.models import F
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def set_updated_at(apps: StateApps, schema_editor): def set_updated_at(apps: StateApps, schema_editor):
SithFile = apps.get_model("core", "SithFile") SithFile = apps.get_model("core", "SithFile")
+4 -10
View File
@@ -19,16 +19,6 @@ from core.utils import get_last_promo, is_image
NonEmptyStr = Annotated[str, MinLen(1)] NonEmptyStr = Annotated[str, MinLen(1)]
class ValidationErrorSchema(Schema):
class ValidationErrorItem(Schema):
loc: list[str | int]
msg: str
type: str
ctx: dict[str, str]
detail: list[ValidationErrorItem]
class UploadedImage(UploadedFile): class UploadedImage(UploadedFile):
@classmethod @classmethod
def _validate(cls, v: Any, info: ValidationInfo) -> Any: def _validate(cls, v: Any, info: ValidationInfo) -> Any:
@@ -161,6 +151,10 @@ class UserFilterSchema(FilterSchema):
return value return value
class MarkdownSchema(Schema):
text: str
class FamilyGodfatherSchema(Schema): class FamilyGodfatherSchema(Schema):
godfather: int godfather: int
godchild: int godchild: int
@@ -25,7 +25,7 @@ export function limitedChoices(Alpine: AlpineType) {
Alpine.directive( Alpine.directive(
"limited-choices", "limited-choices",
(el, { expression }, { evaluateLater, effect }) => { (el, { expression }, { evaluateLater, effect }) => {
const getMaxChoices = evaluateLater<string>(expression); const getMaxChoices = evaluateLater(expression);
let maxChoices: number; let maxChoices: number;
const inputs: HTMLInputElement[] = Array.from( const inputs: HTMLInputElement[] = Array.from(
el.querySelectorAll("input[type='checkbox']"), el.querySelectorAll("input[type='checkbox']"),
@@ -54,7 +54,7 @@ export function limitedChoices(Alpine: AlpineType) {
}); });
} }
effect(() => { effect(() => {
getMaxChoices((value) => { getMaxChoices((value: string) => {
const previousValue = maxChoices; const previousValue = maxChoices;
maxChoices = Number.parseInt(value, 10); maxChoices = Number.parseInt(value, 10);
if (maxChoices < previousValue) { if (maxChoices < previousValue) {
+7 -19
View File
@@ -6,15 +6,10 @@
* for more efficient tree-shaking and gzip compression. * for more efficient tree-shaking and gzip compression.
*/ */
// Must be loaded before Apline
import htmx from "htmx.org";
import "htmx.org/dist/ext/hx-alpine-compat.js";
import "htmx.org/dist/ext/hx-prompt.js";
import "htmx.org/dist/ext/hx-download.js";
import sort from "@alpinejs/sort"; import sort from "@alpinejs/sort";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices"; import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage"; import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar"; import { default as navbar } from "#core:core/navbar";
@@ -48,21 +43,14 @@ polyfillCountryFlagEmojis();
/** /**
* HTMX * HTMX
*/ */
document.body.addEventListener( document.body.addEventListener("htmx:beforeRequest", (event: CustomEvent) => {
"htmx:before:request" as keyof HTMLElementEventMap, event.detail.target.ariaBusy = true;
(event) => { });
(event as CustomEvent).detail.ctx.target.ariaBusy = true;
},
);
document.body.addEventListener( document.body.addEventListener("htmx:beforeSwap", (event: CustomEvent) => {
"htmx:before:swap" as keyof HTMLElementEventMap, event.detail.target.ariaBusy = null;
(event) => { });
(event as CustomEvent).detail.ctx.target.ariaBusy = null;
},
);
htmx.config.transitions = true;
Object.assign(window, { htmx }); Object.assign(window, { htmx });
/** /**
@@ -4,9 +4,9 @@ import type {
TomLoadCallback, TomLoadCallback,
TomOption, TomOption,
TomSettings, TomSettings,
} from "tom-select/src/types"; } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/src/utils"; import type { escape_html } from "tom-select/dist/types/utils";
import { inheritHtmlElement } from "#core:utils/web-components"; import { inheritHtmlElement } from "#core:utils/web-components.ts";
export class AutoCompleteSelectBase extends inheritHtmlElement("select") { export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
static observedAttributes = [ static observedAttributes = [
@@ -15,7 +15,7 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
"max", "max",
"min-characters-for-search", "min-characters-for-search",
]; ];
public widget!: TomSelect; public widget: TomSelect;
protected minCharNumberForSearch = 0; protected minCharNumberForSearch = 0;
protected delay: number | null = null; protected delay: number | null = null;
@@ -24,8 +24,8 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
protected attributeChangedCallback( protected attributeChangedCallback(
name: string, name: string,
_oldValue: string, _oldValue?: string,
newValue: string, newValue?: string,
) { ) {
switch (name) { switch (name) {
case "delay": { case "delay": {
@@ -73,7 +73,7 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
persist: false, persist: false,
maxItems: this.node.multiple ? this.max : 1, maxItems: this.node.multiple ? this.max : 1,
closeAfterSelect: true, closeAfterSelect: true,
loadThrottle: this.delay ?? undefined, loadThrottle: this.delay,
placeholder: this.placeholder, placeholder: this.placeholder,
shouldLoad: (query: string) => this.shouldLoad(query), // wraps the method to avoid shadowing `this` by the one from tom-select shouldLoad: (query: string) => this.shouldLoad(query), // wraps the method to avoid shadowing `this` by the one from tom-select
render: { render: {
@@ -103,7 +103,7 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") {
} }
export abstract class AjaxSelect extends AutoCompleteSelectBase { export abstract class AjaxSelect extends AutoCompleteSelectBase {
protected filter?: (items: TomOption[]) => TomOption[]; protected filter?: (items: TomOption[]) => TomOption[] = null;
protected minCharNumberForSearch = 2; protected minCharNumberForSearch = 2;
/** /**
* A cache of researches that have been made using this input. * A cache of researches that have been made using this input.
@@ -1,12 +1,11 @@
// @ts-expect-error TS2882
import "tom-select/dist/css/tom-select.default.css"; import "tom-select/dist/css/tom-select.default.css";
import type { TomOption } from "tom-select/src/types"; import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/src/utils"; import type { escape_html } from "tom-select/dist/types/utils";
import { import {
AjaxSelect, AjaxSelect,
AutoCompleteSelectBase, AutoCompleteSelectBase,
} from "#core:core/components/ajax-select-base"; } from "#core:core/components/ajax-select-base.ts";
import { registerComponent } from "#core:utils/web-components"; import { registerComponent } from "#core:utils/web-components.ts";
import { import {
type GroupSchema, type GroupSchema,
groupSearchGroup, groupSearchGroup,
@@ -1,14 +1,16 @@
// @ts-expect-error 2307
// biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde // biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
// @ts-expect-error 2307
import "easymde/src/css/easymde.css"; import "easymde/src/css/easymde.css";
import { markdown } from "@ae_utbm/aemark";
// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE // biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE
import type CodeMirror from "codemirror"; // biome-ignore lint/style/useNamingConvention: This is how they called their namespace import type CodeMirror from "codemirror";
// biome-ignore lint/style/useNamingConvention: This is how they called their namespace
import EasyMDE from "easymde"; import EasyMDE from "easymde";
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts";
import { type UploadUploadImageErrors, uploadUploadImage } from "#openapi"; import {
markdownRenderMarkdown,
type UploadUploadImageErrors,
uploadUploadImage,
} from "#openapi";
const loadEasyMde = (textarea: HTMLTextAreaElement) => { const loadEasyMde = (textarea: HTMLTextAreaElement) => {
const easymde = new EasyMDE({ const easymde = new EasyMDE({
@@ -23,11 +25,11 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
file: file, file: file,
}, },
}); });
if (response.response !== undefined && !response.response.ok) { if (!response.response.ok) {
if (response?.response.status === 422) { if (response.response.status === 422) {
onError( onError(
(response.error as UploadUploadImageErrors[422]).detail (response.error as UploadUploadImageErrors[422]).detail
.map((err) => err.ctx.error) .map((err: Record<"ctx", Record<"error", string>>) => err.ctx.error)
.join(" ; "), .join(" ; "),
); );
} else if (response.response.status === 403) { } else if (response.response.status === 403) {
@@ -37,10 +39,6 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
} }
return; return;
} }
if (response.data === undefined) {
// this can't happen, it's just for the type checker to know
return;
}
onSuccess(response.data.href); onSuccess(response.data.href);
// Workaround function to add an image name to uploaded image // Workaround function to add an image name to uploaded image
// Without this, you get ![](url) instead of ![name](url) // Without this, you get ![](url) instead of ![name](url)
@@ -60,7 +58,22 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
}); });
easymde.codemirror.replaceSelection("\n"); easymde.codemirror.replaceSelection("\n");
}, },
previewRender: (plainText) => markdown(plainText), previewRender: (plainText: string, preview: MarkdownInput) => {
/* This is wrapped this way to allow time for Alpine to be loaded on the page */
return Alpine.debounce((plainText: string, preview: MarkdownInput) => {
const func = async (
plainText: string,
preview: MarkdownInput,
): Promise<null> => {
preview.innerHTML = (
await markdownRenderMarkdown({ body: { text: plainText } })
).data as string;
return null;
};
func(plainText, preview);
return null;
}, 300)(plainText, preview);
},
forceSync: true, // Avoid validation error on generic create view forceSync: true, // Avoid validation error on generic create view
imageTexts: { imageTexts: {
sbInit: gettext("Attach files by drag and dropping or pasting from clipboard."), sbInit: gettext("Attach files by drag and dropping or pasting from clipboard."),
@@ -209,11 +222,9 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
}); });
const submits: HTMLInputElement[] = Array.from( const submits: HTMLInputElement[] = Array.from(
(textarea.closest("form") as HTMLFormElement).querySelectorAll( textarea.closest("form").querySelectorAll('input[type="submit"]'),
'input[type="submit"]',
),
); );
const parentDiv = textarea.parentElement?.parentElement as HTMLElement; const parentDiv = textarea.parentElement.parentElement;
function checkMarkdownInput(event: Event) { function checkMarkdownInput(event: Event) {
// an attribute is null if it does not exist, else a string // an attribute is null if it does not exist, else a string
@@ -238,7 +249,6 @@ const loadEasyMde = (textarea: HTMLTextAreaElement) => {
}; };
@registerComponent("markdown-input") @registerComponent("markdown-input")
// biome-ignore lint/correctness/noUnusedVariables: it is used in jinja
class MarkdownInput extends inheritHtmlElement("textarea") { class MarkdownInput extends inheritHtmlElement("textarea") {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -2,7 +2,7 @@ import {
type InheritedHtmlElement, type InheritedHtmlElement,
inheritHtmlElement, inheritHtmlElement,
registerComponent, registerComponent,
} from "#core:utils/web-components"; } from "#core:utils/web-components.ts";
/** /**
* ElementOnce web components * ElementOnce web components
@@ -28,7 +28,7 @@ export function elementOnce<K extends keyof HTMLElementTagNameMap>(tagName: K) {
clearNode() { clearNode() {
while (this.firstChild) { while (this.firstChild) {
this.removeChild(this.lastChild as ChildNode); this.removeChild(this.lastChild);
} }
} }
@@ -130,7 +130,7 @@ startObserver(observer);
export class LinkOnce extends elementOnce("link") { export class LinkOnce extends elementOnce("link") {
getElementQuerySelector(): string { getElementQuerySelector(): string {
// We get href from node.attributes instead of node.href to avoid getting the domain part // We get href from node.attributes instead of node.href to avoid getting the domain part
return `link[href='${this.node.attributes.getNamedItem("href")?.nodeValue}']`; return `link[href='${this.node.attributes.getNamedItem("href").nodeValue}']`;
} }
} }
@@ -142,6 +142,6 @@ export class LinkOnce extends elementOnce("link") {
export class ScriptOnce extends inheritHtmlElement("script") { export class ScriptOnce extends inheritHtmlElement("script") {
getElementQuerySelector(): string { getElementQuerySelector(): string {
// We get href from node.attributes instead of node.src to avoid getting the domain part // We get href from node.attributes instead of node.src to avoid getting the domain part
return `script[src='${this.node.attributes.getNamedItem("src")?.nodeValue}']`; return `script[src='${this.node.attributes.getNamedItem("src").nodeValue}']`;
} }
} }
@@ -1,4 +1,4 @@
import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components.ts";
@registerComponent("nfc-input") @registerComponent("nfc-input")
export class NfcInput extends inheritHtmlElement("input") { export class NfcInput extends inheritHtmlElement("input") {
@@ -26,11 +26,9 @@ export class NfcInput extends inheritHtmlElement("input") {
window.alert(gettext("Unsupported NFC card")); window.alert(gettext("Unsupported NFC card"));
}); });
ndef.addEventListener("reading", (event) => { ndef.addEventListener("reading", (event: NDEFReadingEvent) => {
this.removeAttribute("scan"); this.removeAttribute("scan");
this.node.value = (event as NDEFReadingEvent).serialNumber this.node.value = event.serialNumber.replace(/:/g, "").toUpperCase();
.replace(/:/g, "")
.toUpperCase();
/* Auto submit form, we need another button to not trigger our previously defined click event */ /* Auto submit form, we need another button to not trigger our previously defined click event */
const submit = document.createElement("button"); const submit = document.createElement("button");
this.node.appendChild(submit); this.node.appendChild(submit);
@@ -1,6 +1,6 @@
import { html, render } from "lit-html"; import { html, render } from "lit-html";
import { unsafeHTML } from "lit-html/directives/unsafe-html.js"; import { unsafeHTML } from "lit-html/directives/unsafe-html.js";
import { registerComponent } from "#core:utils/web-components"; import { registerComponent } from "#core:utils/web-components.ts";
@registerComponent("ui-tab") @registerComponent("ui-tab")
export class Tab extends HTMLElement { export class Tab extends HTMLElement {
@@ -19,7 +19,7 @@ export class Tab extends HTMLElement {
} }
if (name === "title") { if (name === "title") {
this.description = newValue ?? ""; this.description = newValue;
} }
this.dispatchEvent(new CustomEvent("ui-tab-updated", { bubbles: true })); this.dispatchEvent(new CustomEvent("ui-tab-updated", { bubbles: true }));
} }
@@ -79,15 +79,15 @@ export class Tab extends HTMLElement {
@registerComponent("ui-tab-group") @registerComponent("ui-tab-group")
export class TabGroup extends HTMLElement { export class TabGroup extends HTMLElement {
private node!: HTMLDivElement; private node: HTMLDivElement;
connectedCallback() { connectedCallback() {
this.node = document.createElement("div"); this.node = document.createElement("div");
this.node.classList.add("tabs", "shadow"); this.node.classList.add("tabs", "shadow");
this.appendChild(this.node); this.appendChild(this.node);
this.addEventListener("ui-tab-activated", (event) => { this.addEventListener("ui-tab-activated", (event: CustomEvent) => {
const target = (event as CustomEvent).detail as Tab; const target = event.detail as Tab;
for (const tab of this.getElementsByTagName("ui-tab") as HTMLCollectionOf<Tab>) { for (const tab of this.getElementsByTagName("ui-tab") as HTMLCollectionOf<Tab>) {
if (tab !== target) { if (tab !== target) {
tab.setActive(false); tab.setActive(false);
@@ -26,23 +26,19 @@ document.addEventListener("alpine:init", () => {
* `counter/templates/counter/product_form.jinja` * `counter/templates/counter/product_form.jinja`
*/ */
Alpine.data("dynamicFormSet", (config?: Config) => ({ Alpine.data("dynamicFormSet", (config?: Config) => ({
formContainer: undefined as unknown as HTMLElement,
nbForms: 0,
template: undefined as unknown as HTMLTemplateElement,
init() { init() {
this.formContainer = this.$refs.formContainer as HTMLElement; this.formContainer = this.$refs.formContainer as HTMLElement;
this.nbForms = this.formContainer.children.length as number; this.nbForms = this.formContainer.children.length as number;
this.template = this.$refs.formTemplate as HTMLTemplateElement; this.template = this.$refs.formTemplate as HTMLTemplateElement;
const prefix = config?.prefix ?? "form"; const prefix = config?.prefix ?? "form";
( this.$root
this.$root.querySelector(`#id_${prefix}-TOTAL_FORMS`) as HTMLFormElement .querySelector(`#id_${prefix}-TOTAL_FORMS`)
).setAttribute(":value", "nbForms"); .setAttribute(":value", "nbForms");
}, },
addForm() { addForm() {
this.formContainer.appendChild(document.importNode(this.template.content, true)); this.formContainer.appendChild(document.importNode(this.template.content, true));
const newForm = this.formContainer.lastElementChild as Element; const newForm = this.formContainer.lastElementChild;
const inputs: NodeListOf<HTMLFormInputElement> = newForm.querySelectorAll( const inputs: NodeListOf<HTMLFormInputElement> = newForm.querySelectorAll(
"input, select, textarea", "input, select, textarea",
); );
@@ -63,7 +59,7 @@ document.addEventListener("alpine:init", () => {
this.nbForms -= 1; this.nbForms -= 1;
// adjust the id of remaining forms // adjust the id of remaining forms
for (let i = 0; i < this.nbForms; i++) { for (let i = 0; i < this.nbForms; i++) {
const form = this.formContainer.children[i]; const form: HTMLDivElement = this.formContainer.children[i];
const inputs: NodeListOf<HTMLFormInputElement> = form.querySelectorAll( const inputs: NodeListOf<HTMLFormInputElement> = form.querySelectorAll(
"input, select, textarea", "input, select, textarea",
); );
+3 -6
View File
@@ -1,5 +1,5 @@
function showMenu() { function showMenu() {
const navbar = document.getElementById("navbar-content") as HTMLElement; const navbar = document.getElementById("navbar-content");
const current = navbar.getAttribute("mobile-display"); const current = navbar.getAttribute("mobile-display");
navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden"); navbar.setAttribute("mobile-display", current === "hidden" ? "revealed" : "hidden");
} }
@@ -20,12 +20,9 @@ function navbarInit() {
item.removeAttribute("open"); item.removeAttribute("open");
} }
}); });
item.addEventListener("click", (event) => { item.addEventListener("click", (event: MouseEvent) => {
// Don't close when clicking on desktop mode // Don't close when clicking on desktop mode
if ( if ((event.target as HTMLElement).nodeName !== "SUMMARY" || event.detail === 0) {
(event.target as HTMLElement).nodeName !== "SUMMARY" ||
(event as MouseEvent).detail === 0
) {
return; return;
} }
@@ -1,4 +1,3 @@
// @ts-expect-error 2307 this dependency does exist, but it's a little bit wacky
import clip from "@arendjr/text-clipper"; import clip from "@arendjr/text-clipper";
/* /*
+7 -8
View File
@@ -70,16 +70,15 @@ function createTooltip(element: HTMLElement) {
function updateTooltip(element: HTMLElement, tooltip: HTMLElement, status: Status) { function updateTooltip(element: HTMLElement, tooltip: HTMLElement, status: Status) {
// Update tooltip status and set it's attributes and content // Update tooltip status and set it's attributes and content
tooltip.setAttribute("tooltip-status", status); tooltip.setAttribute("tooltip-status", status);
tooltip.innerText = element.getAttribute("tooltip") as string; tooltip.innerText = element.getAttribute("tooltip");
for (const attributes of [ for (const attributes of [
{ src: "tooltip-class", dst: "class", default: ["tooltip"] }, { src: "tooltip-class", dst: "class", default: ["tooltip"] },
{ src: "tooltip-id", dst: "id", default: [] }, { src: "tooltip-id", dst: "id", default: [] },
]) { ]) {
const populated = attributes.default; const populated = attributes.default;
const attr = element.getAttribute(attributes.src); if (element.hasAttribute(attributes.src)) {
if (attr !== null) { populated.push(...element.getAttribute(attributes.src).split(" "));
populated.push(...attr.split(" "));
} }
tooltip.setAttribute(attributes.dst, populated.join(" ")); tooltip.setAttribute(attributes.dst, populated.join(" "));
} }
@@ -94,7 +93,7 @@ function getTooltip(element: HTMLElement) {
return tooltip; return tooltip;
} }
function tooltipMouseover(event: Event) { function tooltipMouseover(event: MouseEvent) {
// We get the closest tooltip to have a consistent behavior // We get the closest tooltip to have a consistent behavior
// when hovering over a child element of a tooltip marked element // when hovering over a child element of a tooltip marked element
const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement;
@@ -112,7 +111,7 @@ function tooltipMouseover(event: Event) {
}); });
} }
function tooltipMouseout(event: Event) { function tooltipMouseout(event: MouseEvent) {
// We get the closest tooltip to have a consistent behavior // We get the closest tooltip to have a consistent behavior
// when hovering over a child element of a tooltip marked element // when hovering over a child element of a tooltip marked element
const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement; const target = (event.target as HTMLElement).closest("[tooltip]") as HTMLElement;
@@ -142,7 +141,7 @@ new MutationObserver((mutations: MutationRecord[]) => {
} }
} else if (tooltips.has(target)) { } else if (tooltips.has(target)) {
// Remove corresponding tooltip // Remove corresponding tooltip
tooltips.get(target)?.remove(); tooltips.get(target).remove();
tooltips.delete(target); tooltips.delete(target);
} }
} }
@@ -164,7 +163,7 @@ new MutationObserver((mutations: MutationRecord[]) => {
continue; continue;
} }
if (tooltips.has(target)) { if (tooltips.has(target)) {
tooltips.get(target)?.remove(); tooltips.get(target).remove();
tooltips.delete(target); tooltips.delete(target);
} }
} }
+22 -17
View File
@@ -1,13 +1,20 @@
import cytoscape, { type ElementDefinition, type Singular } from "cytoscape"; import cytoscape, {
type ElementDefinition,
type NodeSingular,
type Singular,
} from "cytoscape";
import cxtmenu from "cytoscape-cxtmenu"; import cxtmenu from "cytoscape-cxtmenu";
import klay, { type KlayLayoutOptions } from "cytoscape-klay"; import klay, { type KlayLayoutOptions } from "cytoscape-klay";
import { History, initialUrlParams, updateQueryString } from "#core:utils/history"; import { History, initialUrlParams, updateQueryString } from "#core:utils/history.ts";
import { familyGetFamilyGraph, type UserProfileSchema } from "#openapi"; import { familyGetFamilyGraph, type UserProfileSchema } from "#openapi";
cytoscape.use(klay); cytoscape.use(klay);
cytoscape.use(cxtmenu); cytoscape.use(cxtmenu);
type GraphData = { data: UserProfileSchema | { source: number; target: number } }[]; type GraphData = (
| { data: UserProfileSchema }
| { data: { source: number; target: number } }
)[];
function isMobile() { function isMobile() {
return window.innerWidth < 500; return window.innerWidth < 500;
@@ -20,8 +27,10 @@ async function getGraphData(
): Promise<GraphData> { ): Promise<GraphData> {
const data = ( const data = (
await familyGetFamilyGraph({ await familyGetFamilyGraph({
path: {
// biome-ignore lint/style/useNamingConvention: api is snake_case // biome-ignore lint/style/useNamingConvention: api is snake_case
path: { user_id: userId }, user_id: userId,
},
query: { query: {
// biome-ignore lint/style/useNamingConvention: api is snake_case // biome-ignore lint/style/useNamingConvention: api is snake_case
godfathers_depth: godfathersDepth, godfathers_depth: godfathersDepth,
@@ -30,10 +39,6 @@ async function getGraphData(
}, },
}) })
).data; ).data;
if (data === undefined) {
console.error("Family graph request failed");
return [];
}
return [ return [
...data.users.map((user) => { ...data.users.map((user) => {
return { data: user }; return { data: user };
@@ -150,7 +155,7 @@ function createGraph(container: HTMLDivElement, data: GraphData, activeUserId: n
{ {
content: '<i class="fa fa-external-link fa-2x"></i>', content: '<i class="fa fa-external-link fa-2x"></i>',
select: (el) => { select: (el) => {
window.open(el.data().profile_url, "_blank")?.focus(); window.open(el.data().profile_url, "_blank").focus();
}, },
}, },
@@ -190,12 +195,12 @@ document.addEventListener("alpine:init", () => {
godfathersDepth: 0, godfathersDepth: 0,
godchildrenDepth: 0, godchildrenDepth: 0,
reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true", reverse: initialUrlParams.get("reverse")?.toLowerCase?.() === "true",
graph: undefined as unknown as cytoscape.Core, graph: undefined as cytoscape.Core,
graphData: {} as GraphData, graphData: {},
isZoomEnabled: !isMobile(), isZoomEnabled: !isMobile(),
getInitialDepth(prop: string) { getInitialDepth(prop: string) {
const value = Number.parseInt(initialUrlParams.get(prop) as string, 10); const value = Number.parseInt(initialUrlParams.get(prop), 10);
if (Number.isNaN(value) || value < config.depthMin || value > config.depthMax) { if (Number.isNaN(value) || value < config.depthMin || value > config.depthMax) {
return defaultDepth; return defaultDepth;
} }
@@ -218,8 +223,8 @@ document.addEventListener("alpine:init", () => {
await delayedFetch(); await delayedFetch();
}); });
} }
this.$watch("reverse", async (newValue, _oldValue) => { this.$watch("reverse", async (value: number) => {
updateQueryString("reverse", newValue.toString(), History.Replace); updateQueryString("reverse", value.toString(), History.Replace);
await this.reverseGraph(); await this.reverseGraph();
}); });
this.$watch("graphData", async () => { this.$watch("graphData", async () => {
@@ -254,9 +259,9 @@ document.addEventListener("alpine:init", () => {
}, },
async reverseGraph() { async reverseGraph() {
this.graph this.graph.elements((el: NodeSingular) => {
.elements() el.position({ x: -el.position().x, y: -el.position().y });
.positions((el, _) => ({ x: -el.position().x, y: -el.position().y })); });
this.graph.center(this.graph.elements()); this.graph.center(this.graph.elements());
}, },
+2 -2
View File
@@ -5,9 +5,9 @@ interface AlertParams {
export class AlertMessage { export class AlertMessage {
public open: boolean; public open: boolean;
public success!: boolean; public success: boolean;
public content: string; public content: string;
private timeoutId: number | null; private timeoutId?: number;
private readonly defaultDuration: number; private readonly defaultDuration: number;
constructor(params?: { defaultDuration: number }) { constructor(params?: { defaultDuration: number }) {
+5 -8
View File
@@ -37,10 +37,6 @@ export const paginated = async <T>(
queryParams.query.page = 1; queryParams.query.page = 1;
const firstPage = (await endpoint(queryParams)).data; const firstPage = (await endpoint(queryParams)).data;
if (firstPage === undefined) {
console.error(`Request to "${options?.url}" failed`);
return [];
}
const results = firstPage.results; const results = firstPage.results;
const nbElements = firstPage.count; const nbElements = firstPage.count;
@@ -49,9 +45,9 @@ export const paginated = async <T>(
if (nbPages > 1) { if (nbPages > 1) {
const promises: Promise<T[]>[] = []; const promises: Promise<T[]>[] = [];
for (let i = 2; i <= nbPages; i++) { for (let i = 2; i <= nbPages; i++) {
const nextPage = structuredClone(queryParams) as Required<PaginatedRequest>; const nextPage = structuredClone(queryParams);
nextPage.query.page = i; nextPage.query.page = i;
promises.push(endpoint(nextPage).then((res) => res.data?.results ?? [])); promises.push(endpoint(nextPage).then((res) => res.data.results));
} }
results.push(...(await Promise.all(promises)).flat()); results.push(...(await Promise.all(promises)).flat());
} }
@@ -65,7 +61,8 @@ interface Request extends TDataShape {
interface InterceptorOptions { interface InterceptorOptions {
url: string; url: string;
} }
export type GenericEndpoint = <ThrowOnError extends boolean = false>(
type GenericEndpoint = <ThrowOnError extends boolean = false>(
options?: Options<Request, ThrowOnError>, options?: Options<Request, ThrowOnError>,
) => RequestResult<unknown, unknown, ThrowOnError>; ) => RequestResult<unknown, unknown, ThrowOnError>;
@@ -74,7 +71,7 @@ export type GenericEndpoint = <ThrowOnError extends boolean = false>(
**/ **/
export const makeUrl = async (endpoint: GenericEndpoint) => { export const makeUrl = async (endpoint: GenericEndpoint) => {
let url = ""; let url = "";
const interceptor = (_request: Request, options: InterceptorOptions) => { const interceptor = (_request: undefined, options: InterceptorOptions) => {
url = options.url; url = options.url;
throw new Error("We don't want to send the request"); throw new Error("We don't want to send the request");
}; };
+9 -12
View File
@@ -7,14 +7,14 @@ interface StringifyOptions<T extends object> {
titleRow?: readonly string[]; titleRow?: readonly string[];
} }
function getNested<T extends { [key: string]: unknown }>(obj: T, key: NestedKeyOf<T>) { function getNested<T extends object>(obj: T, key: NestedKeyOf<T>) {
const path = key.split("."); const path: (keyof object)[] = key.split(".") as (keyof unknown)[];
let res = obj[path.shift() as string] as { [key: string]: unknown } | undefined; let res = obj[path.shift() as keyof T];
for (const node of path) { for (const node of path) {
if (res === undefined) { if (res === null) {
break; break;
} }
res = res[node] as { [key: string]: unknown } | undefined; res = res[node];
} }
return res; return res;
} }
@@ -29,21 +29,18 @@ function sanitizeCell(content: string): string {
} }
export const csv = { export const csv = {
stringify: <T extends { [key: string]: unknown }>( stringify: <T extends object>(objs: T[], options?: StringifyOptions<T>) => {
objs: T[], const columns = options.columns;
options?: StringifyOptions<T>,
) => {
const columns = options?.columns;
const content = objs const content = objs
.map((obj) => { .map((obj) => {
return (columns ?? []) return columns
.map((col) => { .map((col) => {
return sanitizeCell((getNested(obj, col) ?? "").toString()); return sanitizeCell((getNested(obj, col) ?? "").toString());
}) })
.join(","); .join(",");
}) })
.join("\n"); .join("\n");
if (!options?.titleRow) { if (!options.titleRow) {
return content; return content;
} }
const firstRow = options.titleRow.map(sanitizeCell).join(","); const firstRow = options.titleRow.map(sanitizeCell).join(",");
+2 -3
View File
@@ -29,7 +29,6 @@ export function registerComponent(name: string, options?: ElementDefinitionOptio
export interface InheritedHtmlElement<K extends keyof HTMLElementTagNameMap> export interface InheritedHtmlElement<K extends keyof HTMLElementTagNameMap>
extends HTMLElement { extends HTMLElement {
readonly inheritedTagName: K; readonly inheritedTagName: K;
// readonly initializedAttribute: "component-initialized";
node: HTMLElementTagNameMap[K]; node: HTMLElementTagNameMap[K];
} }
@@ -48,8 +47,8 @@ export function inheritHtmlElement<K extends keyof HTMLElementTagNameMap>(tagNam
implements InheritedHtmlElement<K> implements InheritedHtmlElement<K>
{ {
readonly inheritedTagName = tagName; readonly inheritedTagName = tagName;
readonly initializedAttribute = "component-initialized"; private readonly initializedAttribute = "component-initialized";
node!: HTMLElementTagNameMap[K]; node: HTMLElementTagNameMap[K];
connectedCallback(autoAddNode?: boolean) { connectedCallback(autoAddNode?: boolean) {
// When nesting inherited elements, we might trigger the wrapping twice // When nesting inherited elements, we might trigger the wrapping twice
-4
View File
@@ -4,10 +4,6 @@
<div id="page"> <div id="page">
<h3>{% trans %}404, Not Found{% endtrans %}</h3> <h3>{% trans %}404, Not Found{% endtrans %}</h3>
<blockquote>
{% trans %}Impossible, perhaps the archives are incomplete{% endtrans %}
</blockquote>
</div> </div>
{% endblock %} {% endblock %}
@@ -31,7 +31,7 @@
{% if is_fragment %} {% if is_fragment %}
hx-post="{{ action }}" hx-post="{{ action }}"
hx-target="#content" hx-target="#content"
hx-swap="innerHTML" hx-swap="outerHtml"
{% endif %} {% endif %}
>{% trans %}Confirm{% endtrans %}</button> >{% trans %}Confirm{% endtrans %}</button>
@@ -39,7 +39,7 @@
{% if is_fragment %} {% if is_fragment %}
hx-get="{{ previous }}" hx-get="{{ previous }}"
hx-target="#content" hx-target="#content"
hx-swap="innerHTML" hx-swap="outerHtml"
{% else %} {% else %}
action="window.history.back()" action="window.history.back()"
{% endif %} {% endif %}
+2 -2
View File
@@ -39,13 +39,13 @@
<p><button <p><button
hx-get="{{ url('core:file_moderate', file_id=f.id) }}" hx-get="{{ url('core:file_moderate', file_id=f.id) }}"
hx-target="#content" hx-target="#content"
hx-swap="innerHTML" hx-swap="outerHtml"
>{% trans %}Moderate{% endtrans %}</button> - >{% trans %}Moderate{% endtrans %}</button> -
{% set current_page = url('core:file_moderation') + "?page=" + page_obj.number | string %} {% set current_page = url('core:file_moderation') + "?page=" + page_obj.number | string %}
<button <button
hx-get="{{ url('core:file_delete', file_id=f.id) }}?next={{ current_page | urlencode }}&previous={{ current_page | urlencode }}" hx-get="{{ url('core:file_delete', file_id=f.id) }}?next={{ current_page | urlencode }}&previous={{ current_page | urlencode }}"
hx-target="#file-{{ loop.index }}" hx-target="#file-{{ loop.index }}"
hx-swap="innerHTML" hx-swap="outerHtml"
>{% trans %}Delete{% endtrans %}</button></p> >{% trans %}Delete{% endtrans %}</button></p>
</div> </div>
{% endfor %} {% endfor %}
@@ -1,6 +1,6 @@
<form <form
hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}" hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}"
hx-disable="find input[type='submit']" hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }" hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }"
> >
{% for message in messages %} {% for message in messages %}
+2 -4
View File
@@ -20,15 +20,13 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # 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 import honeypot.templatetags.honeypot as honeypot_filters
from django.template.loader import render_to_string from django.template.loader import render_to_string
from jinja2 import Environment, nodes from jinja2 import Environment, nodes
from jinja2.ext import Extension from jinja2.ext import Extension
from jinja2.parser import Parser
if TYPE_CHECKING:
from jinja2.parser import Parser
class HoneypotExtension(Extension): class HoneypotExtension(Extension):
+1 -4
View File
@@ -23,18 +23,15 @@
# #
import datetime import datetime
from typing import TYPE_CHECKING
import phonenumbers import phonenumbers
from aemark import markdown as md from aemark import markdown as md
from django import template from django import template
from django.forms import BoundField
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ngettext from django.utils.translation import ngettext
if TYPE_CHECKING:
from django.forms import BoundField
register = template.Library() register = template.Library()
+1 -4
View File
@@ -1,5 +1,4 @@
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
import freezegun import freezegun
import pytest import pytest
@@ -7,6 +6,7 @@ from aemark import markdown
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from model_bakery import baker 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.baker_recipes import board_user, subscriber_user
from core.models import AnonymousUser, Page, PageRev, User from core.models import AnonymousUser, Page, PageRev, User
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
class TestEditPage: class TestEditPage:
-16
View File
@@ -141,22 +141,6 @@ class TestSearchUsersView(TestSearchUsers):
response = self.client.get(reverse("core:search")) response = self.client.get(reverse("core:search"))
assert response.status_code == 200 assert response.status_code == 200
def test_search_with_whitelist_unique(self):
"""Test that when a user has a whitelist and appears in the results,
it appears only once.
This is a regression test (cf #1463)
"""
user = subscriber_user.make(is_viewable=False)
user.whitelisted_users.add(
*subscriber_user.make(_quantity=4, _bulk_create=True)
)
self.client.force_login(user)
response = self.client.get(
reverse("core:search", query={"query": user.last_name})
)
assert response.context_data["users"] == [user]
@pytest.mark.django_db @pytest.mark.django_db
def test_user_account_not_found(client: Client): def test_user_account_not_found(client: Client):
+3 -5
View File
@@ -17,18 +17,16 @@ from datetime import date, timedelta
# Image utils # Image utils
from io import BytesIO from io import BytesIO
from typing import TYPE_CHECKING, Final from typing import Final
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 django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest
from django.utils.timezone import localdate from django.utils.timezone import localdate
from PIL.Image import Image, Resampling from PIL.Image import Image, Resampling
if TYPE_CHECKING:
from django.core.files.uploadedfile import UploadedFile
from django.http import HttpRequest
RED_PIXEL_PNG: Final[bytes] = ( RED_PIXEL_PNG: Final[bytes] = (
b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52"
b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53" b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53"
+1 -4
View File
@@ -13,7 +13,7 @@
# #
# #
import mimetypes import mimetypes
from typing import TYPE_CHECKING from pathlib import Path
from urllib.parse import quote, urljoin from urllib.parse import quote, urljoin
# This file contains all the views that concern the page model # 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 from counter.utils import is_logged_in_counter
if TYPE_CHECKING:
from pathlib import Path
def send_raw_file(path: Path) -> HttpResponse: def send_raw_file(path: Path) -> HttpResponse:
"""Send a file located in the MEDIA_ROOT """Send a file located in the MEDIA_ROOT
+1 -4
View File
@@ -22,8 +22,8 @@
# #
import re import re
from copy import copy from copy import copy
from datetime import date, datetime
from io import BytesIO from io import BytesIO
from typing import TYPE_CHECKING
from captcha.fields import CaptchaField from captcha.fields import CaptchaField
from django import forms from django import forms
@@ -59,9 +59,6 @@ from core.views.widgets.ajax_select import (
) )
from core.views.widgets.markdown import MarkdownInput from core.views.widgets.markdown import MarkdownInput
if TYPE_CHECKING:
from datetime import date, datetime
# Widgets # Widgets
+2 -7
View File
@@ -22,11 +22,11 @@
# #
# #
from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import F 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.shortcuts import get_object_or_404, redirect
from django.views.generic import ListView, TemplateView from django.views.generic import ListView, TemplateView
@@ -34,10 +34,6 @@ from club.models import Club
from core.models import Notification, User from core.models import Notification, User
from core.schemas import UserFilterSchema from core.schemas import UserFilterSchema
if TYPE_CHECKING:
from django.db.models.query import QuerySet
from django.http import HttpRequest
class NotificationList(LoginRequiredMixin, ListView): class NotificationList(LoginRequiredMixin, ListView):
model = Notification model = Notification
@@ -69,7 +65,6 @@ class SearchView(LoginRequiredMixin, TemplateView):
UserFilterSchema(search=query) UserFilterSchema(search=query)
.filter(User.objects.viewable_by(self.request.user)) .filter(User.objects.viewable_by(self.request.user))
.order_by(F("last_login").desc(nulls_last=True)) .order_by(F("last_login").desc(nulls_last=True))
.distinct()
) )
clubs = list(Club.objects.filter(name__icontains=query)[:5]) clubs = list(Club.objects.filter(name__icontains=query)[:5])
return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs} return super().get_context_data(**kwargs) | {"users": users, "clubs": clubs}
+3 -5
View File
@@ -1,16 +1,14 @@
import copy import copy
import inspect 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.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.safestring import SafeString
from django.views import View from django.views import View
from django.views.generic.base import ContextMixin, TemplateResponseMixin 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): class TabedViewMixin(View):
"""Basic functions for displaying tabs in the template.""" """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 # This file contains all the views that concern the user model
from operator import itemgetter from operator import itemgetter
from smtplib import SMTPException from smtplib import SMTPException
from typing import TYPE_CHECKING
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login, views 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.template.loader import render_to_string
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
@@ -73,9 +73,6 @@ from counter.models import Refilling, Selling
from eboutic.models import Invoice from eboutic.models import Invoice
from trombi.views import UserTrombiForm from trombi.views import UserTrombiForm
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class SithLoginView(views.LoginView): class SithLoginView(views.LoginView):
"""The login View.""" """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.contrib.staticfiles.storage import staticfiles_storage
from django.db.models import Model, QuerySet
from django.forms import Select, SelectMultiple from django.forms import Select, SelectMultiple
from ninja import ModelSchema
from pydantic import TypeAdapter from pydantic import TypeAdapter
from core.models import Group, SithFile, User from core.models import Group, SithFile, User
from core.schemas import GroupSchema, SithFileSchema, UserProfileSchema 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: class AutoCompleteSelectMixin:
component_name = "autocomplete-select" component_name = "autocomplete-select"
+2 -4
View File
@@ -3,7 +3,7 @@ import math
import uuid import uuid
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from typing import TYPE_CHECKING, ClassVar from typing import ClassVar
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django import forms from django import forms
@@ -11,6 +11,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.forms import BaseModelFormSet from django.forms import BaseModelFormSet
from django.http import HttpRequest
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -58,9 +59,6 @@ from counter.widgets.ajax_select import (
AutoCompleteSelectProduct, AutoCompleteSelectProduct,
) )
if TYPE_CHECKING:
from django.http import HttpRequest
class BillingInfoForm(forms.ModelForm): class BillingInfoForm(forms.ModelForm):
class Meta: class Meta:
+1 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from operator import attrgetter from operator import attrgetter
from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.core.mail import send_mass_mail 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 core.models import User, UserQuerySet
from counter.models import AccountDump, Counter, Customer, Product, Selling from counter.models import AccountDump, Counter, Customer, Product, Selling
if TYPE_CHECKING:
from collections.abc import Iterable
class Command(BaseCommand): class Command(BaseCommand):
"""Effectively dump the inactive users. """Effectively dump the inactive users.
+2 -3
View File
@@ -1,14 +1,13 @@
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable
from django.http import HttpRequest, HttpResponse
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from core.models import User
from counter.models import Permanency from counter.models import Permanency
if TYPE_CHECKING: if TYPE_CHECKING:
from django.contrib.sessions.backends.base import SessionBase from django.contrib.sessions.backends.base import SessionBase
from django.http import HttpRequest, HttpResponse
from core.models import User
SESSION_PERMANENCES_KEY = "permanence_ids" SESSION_PERMANENCES_KEY = "permanence_ids"
@@ -1,14 +1,11 @@
# Generated by Django 4.2.17 on 2024-12-08 13:30 # Generated by Django 4.2.17 on 2024-12-08 13:30
from operator import attrgetter from operator import attrgetter
from typing import TYPE_CHECKING
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Count from django.db.models import Count
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def delete_duplicates(apps: StateApps, schema_editor): def delete_duplicates(apps: StateApps, schema_editor):
"""Delete cards of users with more than one student cards. """Delete cards of users with more than one student cards.
@@ -1,11 +1,7 @@
# Generated by Django 4.2.17 on 2024-12-15 17:53 # Generated by Django 4.2.17 on 2024-12-15 17:53
from typing import TYPE_CHECKING
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def move_priority_to_order(apps: StateApps, schema_editor): def move_priority_to_order(apps: StateApps, schema_editor):
@@ -1,13 +1,9 @@
# Generated by Django 5.2.8 on 2025-11-19 17:59 # 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 import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Case, When from django.db.models import Case, When
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def migrate_selling_payment_method(apps: StateApps, schema_editor): def migrate_selling_payment_method(apps: StateApps, schema_editor):
# 0 <=> SITH_ACCOUNT is the default value, so no need to migrate it # 0 <=> SITH_ACCOUNT is the default value, so no need to migrate it
@@ -1,16 +1,13 @@
# Generated by Django 5.2.8 on 2026-02-10 15:40 # Generated by Django 5.2.8 on 2026-02-10 15:40
from operator import attrgetter from operator import attrgetter
from typing import TYPE_CHECKING
import django.utils.timezone import django.utils.timezone
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import OuterRef, Subquery from django.db.models import OuterRef, Subquery
from counter.models import Selling from counter.models import Selling
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def apply_product_history_dates(apps: StateApps, schema_editor): def apply_product_history_dates(apps: StateApps, schema_editor):
"""Approximate a posteriori the value of created_at and updated_at.""" """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 # Generated by Django 5.2.11 on 2026-02-18 13:30
from typing import TYPE_CHECKING
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.state import StateApps
import counter.fields import counter.fields
if TYPE_CHECKING:
from django.db.migrations.state import StateApps
def migrate_prices(apps: StateApps, schema_editor): def migrate_prices(apps: StateApps, schema_editor):
Product = apps.get_model("counter", "Product") Product = apps.get_model("counter", "Product")
@@ -1,7 +1,7 @@
import type { TomOption } from "tom-select/src/types"; import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/src/utils"; import type { escape_html } from "tom-select/dist/types/utils";
import { AjaxSelect } from "#core:core/components/ajax-select-base"; import { AjaxSelect } from "#core:core/components/ajax-select-base.ts";
import { registerComponent } from "#core:utils/web-components"; import { registerComponent } from "#core:utils/web-components.ts";
import { import {
type CounterSchema, type CounterSchema,
counterSearchCounter, counterSearchCounter,
@@ -28,23 +28,14 @@ export class ProductAjaxSelect extends AjaxSelect {
return []; return [];
} }
// In the context in which this method is called, `this` might be shadowed
// We need to call it explicitly from the class itself
private static getName(
item: SimpleProductSchema,
sanitize: typeof escape_html,
): string {
return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name;
}
protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<div class="select-item"> return `<div class="select-item">
<span class="select-item-text">${ProductAjaxSelect.getName(item, sanitize)}</span> <span class="select-item-text">${sanitize(item.code)} - ${sanitize(item.name)}</span>
</div>`; </div>`;
} }
protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<span>${ProductAjaxSelect.getName(item, sanitize)}</span>`; return `<span>${sanitize(item.code)} - ${sanitize(item.name)}</span>`;
} }
} }
@@ -53,7 +44,7 @@ export class ProductTypeAjaxSelect extends AjaxSelect {
protected valueField = "id"; protected valueField = "id";
protected labelField = "name"; protected labelField = "name";
protected searchField = ["name"]; protected searchField = ["name"];
private productTypes = null as ProductTypeSchema[] | null; private productTypes = null as ProductTypeSchema[];
protected async search(query: string): Promise<TomOption[]> { protected async search(query: string): Promise<TomOption[]> {
// The production database has a grand total of 26 product types // The production database has a grand total of 26 product types
@@ -61,7 +52,7 @@ export class ProductTypeAjaxSelect extends AjaxSelect {
// Thus, it's appropriate to fetch all product types during first use, // Thus, it's appropriate to fetch all product types during first use,
// then to reuse the result again and again. // then to reuse the result again and again.
if (this.productTypes === null) { if (this.productTypes === null) {
this.productTypes = (await producttypeFetchAll()).data || []; this.productTypes = (await producttypeFetchAll()).data || null;
} }
return this.productTypes.filter((t) => return this.productTypes.filter((t) =>
t.name.toLowerCase().includes(query.toLowerCase()), t.name.toLowerCase().includes(query.toLowerCase()),
@@ -59,7 +59,7 @@ export class CounterProductSelect extends AutoCompleteSelectBase {
return onOptionSelect.call( return onOptionSelect.call(
this.widget, this.widget,
evt, evt,
this.widget.getOption(value, true) as HTMLElement, this.widget.getOption(value, true),
); );
}, },
); );
@@ -70,15 +70,12 @@ export class CounterProductSelect extends AutoCompleteSelectBase {
...super.tomSelectSettings(), ...super.tomSelectSettings(),
openOnFocus: false, openOnFocus: false,
// We make searching on exact code matching a higher priority // We make searching on exact code matching a higher priority
// We need to manually set weights, or it results on an inconsistent // We need to manually set weights or it results on an inconsistent
// behavior between production and development environment // behavior between production and development environment
//
// SearchField can be an object array (according to the docs),
// but it is unproperly typed, so we must force-cast to trick TS
searchField: [ searchField: [
{ field: "code", weight: 2 }, { field: "code", weight: 2 },
{ field: "text", weight: 0.5 }, { field: "text", weight: 0.5 },
] as unknown as string[], ],
}; };
} }
} }
@@ -1,14 +1,9 @@
import { showSaveFilePicker } from "native-file-system-adapter"; import { showSaveFilePicker } from "native-file-system-adapter";
import type TomSelect from "tom-select"; import type TomSelect from "tom-select";
import type { ClubAjaxSelect } from "#club:club/components/ajax-select-index";
import type { NestedKeyOf } from "#core:types/nested-key"; import type { NestedKeyOf } from "#core:types/nested-key";
import { type PaginatedRequest, paginated } from "#core:utils/api"; import { paginated } from "#core:utils/api";
import { csv } from "#core:utils/csv"; import { csv } from "#core:utils/csv";
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history"; import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
import type {
CounterAjaxSelect,
ProductTypeAjaxSelect,
} from "#counter:counter/components/ajax-select-index";
import { import {
type ProductSchema, type ProductSchema,
type ProductSearchProductsDetailedData, type ProductSearchProductsDetailedData,
@@ -56,8 +51,6 @@ const csvColumnTitles = [
gettext("archived"), gettext("archived"),
]; ];
type ProductStatus = "active" | "archived" | "both";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
Alpine.data("productList", () => ({ Alpine.data("productList", () => ({
loading: false, loading: false,
@@ -66,7 +59,8 @@ document.addEventListener("alpine:init", () => {
/** Total number of elements corresponding to the current query. */ /** Total number of elements corresponding to the current query. */
nbPages: 0, nbPages: 0,
productStatus: "" as ProductStatus,
productStatus: "" as "active" | "archived" | "both",
search: "", search: "",
productTypes: [] as string[], productTypes: [] as string[],
clubs: [] as string[], clubs: [] as string[],
@@ -77,18 +71,16 @@ document.addEventListener("alpine:init", () => {
async init() { async init() {
const url = getCurrentUrlParams(); const url = getCurrentUrlParams();
this.search = url.get("search") || ""; this.search = url.get("search") || "";
this.productStatus = (url.get("productStatus") ?? "active") as ProductStatus; this.productStatus = url.get("productStatus") ?? "active";
const productTypesWidget = (this.$refs.productTypesInput as ProductTypeAjaxSelect) const productTypesWidget = this.$refs.productTypesInput.widget as TomSelect;
.widget as TomSelect;
productTypesWidget.on("change", (items: string[]) => { productTypesWidget.on("change", (items: string[]) => {
this.productTypes = [...items]; this.productTypes = [...items];
}); });
const clubsWidget = (this.$refs.clubsInput as ClubAjaxSelect).widget as TomSelect; const clubsWidget = this.$refs.clubsInput.widget as TomSelect;
clubsWidget.on("change", (items: string[]) => { clubsWidget.on("change", (items: string[]) => {
this.clubs = [...items]; this.clubs = [...items];
}); });
const countersWidget = (this.$refs.countersInput as CounterAjaxSelect) const countersWidget = this.$refs.countersInput.widget as TomSelect;
.widget as TomSelect;
countersWidget.on("change", (items: string[]) => { countersWidget.on("change", (items: string[]) => {
this.counters = [...items]; this.counters = [...items];
}); });
@@ -135,9 +127,9 @@ document.addEventListener("alpine:init", () => {
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
is_archived: isArchived, is_archived: isArchived,
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
product_type: [...this.productTypes.map(Number.parseInt)], product_type: [...this.productTypes],
club: [...this.clubs.map(Number.parseInt)], club: [...this.clubs],
counter: [...this.counters.map(Number.parseInt)], counter: [...this.counters],
}, },
}; };
}, },
@@ -149,10 +141,6 @@ document.addEventListener("alpine:init", () => {
this.loading = true; this.loading = true;
const options = this.getQueryParams(); const options = this.getQueryParams();
const resp = await productSearchProductsDetailed(options); const resp = await productSearchProductsDetailed(options);
if (resp.data === undefined) {
console.error("Product search request failed");
return;
}
this.nbPages = Math.ceil(resp.data.count / defaultPageSize); this.nbPages = Math.ceil(resp.data.count / defaultPageSize);
this.products = resp.data.results.reduce<GroupedProducts>( this.products = resp.data.results.reduce<GroupedProducts>(
(acc: GroupedProducts, curr: ProductSchema) => { (acc: GroupedProducts, curr: ProductSchema) => {
@@ -184,10 +172,7 @@ document.addEventListener("alpine:init", () => {
// If not, fetch them. // If not, fetch them.
const products: ProductSchema[] = const products: ProductSchema[] =
this.nbPages > 1 this.nbPages > 1
? await paginated( ? await paginated(productSearchProductsDetailed, this.getQueryParams())
productSearchProductsDetailed,
this.getQueryParams() as PaginatedRequest,
)
: Object.values<ProductSchema[]>(this.products).flat(); : Object.values<ProductSchema[]>(this.products).flat();
// CSV cannot represent nested data // CSV cannot represent nested data
// so we create a row for each price of each product. // so we create a row for each price of each product.
@@ -1,5 +1,5 @@
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { AlertMessage } from "#core:utils/alert-message"; import { AlertMessage } from "#core:utils/alert-message.ts";
import { producttypeReorder } from "#openapi"; import { producttypeReorder } from "#openapi";
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
@@ -22,7 +22,7 @@ document.addEventListener("alpine:init", () => {
const productTypes = this.$refs.productTypes const productTypes = this.$refs.productTypes
.childNodes as NodeListOf<HTMLLIElement>; .childNodes as NodeListOf<HTMLLIElement>;
const getId = (elem: HTMLLIElement) => const getId = (elem: HTMLLIElement) =>
Number.parseInt(elem.getAttribute("x-sort:item") as string, 10); Number.parseInt(elem.getAttribute("x-sort:item"), 10);
const query = const query =
newPosition === 0 newPosition === 0
? { above: getId(productTypes.item(1)) } ? { above: getId(productTypes.item(1)) }
@@ -32,10 +32,6 @@ document.addEventListener("alpine:init", () => {
path: { type_id: itemId }, path: { type_id: itemId },
query: query, query: query,
}); });
if (response.response === undefined) {
console.error("Product type reordering request failed");
return;
}
this.openAlertMessage(response.response); this.openAlertMessage(response.response);
this.loading = false; this.loading = false;
}, },
@@ -1,10 +1,5 @@
<div id="student_card_form"> <div id="student_card_form">
<form <form hx-post="{{ action }}" hx-swap="outerHTML" hx-target="#student_card_form">
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
hx-disable="input[type='submit']"
>
{% csrf_token %} {% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p> <p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" /> <input type="submit" value="{% trans %}Confirm{% endtrans %}" />
+1 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
import freezegun import freezegun
import pytest 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 counter.models import AccountDump, Customer, Refilling, Selling
from subscription.models import Subscription from subscription.models import Subscription
if TYPE_CHECKING:
from collections.abc import Iterable
class TestAccountDump(TestCase): class TestAccountDump(TestCase):
@classmethod @classmethod
+1 -4
View File
@@ -1,9 +1,9 @@
import json import json
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from django_celery_beat.models import ClockedSchedule from django_celery_beat.models import ClockedSchedule
@@ -18,9 +18,6 @@ from counter.forms import (
) )
from counter.models import Product, ProductType, ScheduledProductAction from counter.models import Product, ProductType, ScheduledProductAction
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_edit_product(client: Client): def test_edit_product(client: Client):
+1 -26
View File
@@ -15,7 +15,6 @@
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING
import pytest import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@@ -23,6 +22,7 @@ from dateutil.relativedelta import relativedelta
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Permission, make_password from django.contrib.auth.models import Permission, make_password
from django.contrib.messages import DEFAULT_LEVELS, get_messages from django.contrib.messages import DEFAULT_LEVELS, get_messages
from django.http import HttpResponse
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
@@ -48,9 +48,6 @@ from counter.models import (
Selling, Selling,
) )
if TYPE_CHECKING:
from django.http import HttpResponse
def set_age(user: User, age: int): def set_age(user: User, age: int):
user.date_of_birth = localdate().replace(year=localdate().year - age) user.date_of_birth = localdate().replace(year=localdate().year - age)
@@ -889,28 +886,6 @@ class TestBarmanConnection(TestCase):
) )
assert response.status_code == 403 assert response.status_code == 403
def test_logout_then_login(self):
"""Test that the login is successful if it is after a previous logout.
This is a regression test for #1440
"""
self.client.post( # login
self.login_url, {"username": self.barman.username, "password": "plop"}
)
self.client.post( # logout
reverse("counter:logout", kwargs={"counter_id": self.counter.id}),
data={"user_id": self.barman.id},
)
response = self.client.post( # and re-login
self.login_url, {"username": self.barman.username, "password": "plop"}
)
assert response.status_code == 200
assert response.headers["HX-Redirect"] == self.detail_url
response = self.client.get(
self.detail_url, {"username": self.barman.username, "password": "plop"}
)
assert self.barman in response.wsgi_request.barmen
@pytest.mark.django_db @pytest.mark.django_db
def test_barman_timeout(client: Client): def test_barman_timeout(client: Client):
+1 -4
View File
@@ -1,10 +1,10 @@
from datetime import date, datetime from datetime import date, datetime
from typing import TYPE_CHECKING
import pytest import pytest
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from model_bakery import baker from model_bakery import baker
@@ -16,9 +16,6 @@ from counter.baker_recipes import sale_recipe
from counter.forms import InvoiceCallForm from counter.forms import InvoiceCallForm
from counter.models import Customer, InvoiceCall, Selling from counter.models import Customer, InvoiceCall, Selling
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.parametrize( @pytest.mark.parametrize(
+1 -5
View File
@@ -1,7 +1,6 @@
from typing import TYPE_CHECKING
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker, seq 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 core.models import Group, User
from counter.models import ProductType from counter.models import ProductType
if TYPE_CHECKING:
from django.test import Client
@pytest.fixture @pytest.fixture
def product_types(db) -> list[ProductType]: def product_types(db) -> list[ProductType]:
+1 -4
View File
@@ -1,11 +1,8 @@
from typing import TYPE_CHECKING
from urllib.parse import urlparse from urllib.parse import urlparse
from django.http import HttpRequest
from django.urls import resolve from django.urls import resolve
if TYPE_CHECKING:
from django.http import HttpRequest
def is_logged_in_counter(request: HttpRequest) -> bool: def is_logged_in_counter(request: HttpRequest) -> bool:
"""Check if the request is sent from a device logged to a counter. """Check if the request is sent from a device logged to a counter.
+1 -5
View File
@@ -13,8 +13,6 @@
# #
# #
from typing import TYPE_CHECKING
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin 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 import DetailView, ListView
from django.views.generic.edit import BaseFormView, UpdateView from django.views.generic.edit import BaseFormView, UpdateView
from core.models import User
from counter.forms import CashSummaryFormBase from counter.forms import CashSummaryFormBase
from counter.models import ( from counter.models import (
CashRegisterSummary, CashRegisterSummary,
@@ -36,9 +35,6 @@ from counter.views.mixins import (
CounterTabsMixin, CounterTabsMixin,
) )
if TYPE_CHECKING:
from core.models import User
class CashRegisterSummaryForm(forms.Form): class CashRegisterSummaryForm(forms.Form):
"""Provide the cash summary form.""" """Provide the cash summary form."""
+3 -7
View File
@@ -14,7 +14,6 @@
# #
import random import random
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@@ -23,11 +22,14 @@ from django.db.models import Q
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, resolve_url from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import CreateView, FormView from django.views.generic import CreateView, FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from ninja.main import HttpRequest
from core.auth.mixins import CanViewMixin from core.auth.mixins import CanViewMixin
from core.models import User
from core.views.mixins import FragmentMixin, UseFragmentsMixin from core.views.mixins import FragmentMixin, UseFragmentsMixin
from counter.forms import BasketForm, RefillForm from counter.forms import BasketForm, RefillForm
from counter.models import ( 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.mixins import CounterTabsMixin
from counter.views.student_card import StudentCardFormFragment 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: def get_operator(request: HttpRequest, counter: Counter, customer: Customer) -> User:
if counter.type != "BAR": if counter.type != "BAR":
+1 -5
View File
@@ -13,7 +13,6 @@
# #
# #
from datetime import timedelta from datetime import timedelta
from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@@ -22,6 +21,7 @@ from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import SafeString
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import DetailView from django.views.generic import DetailView
from django.views.generic.detail import SingleObjectMixin 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.utils import is_logged_in_counter
from counter.views.mixins import CounterTabsMixin from counter.views.mixins import CounterTabsMixin
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView): class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
model = Counter model = Counter
@@ -64,7 +61,6 @@ class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
user = form.get_user() user = form.get_user()
perm = self.object.permanencies.create(user=user, start=timezone.now()) perm = self.object.permanencies.create(user=user, start=timezone.now())
self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id) self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id)
self.request.session.modified = True
self.success_url = reverse( self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.id} "counter:details", kwargs={"counter_id": self.object.id}
) )
+1 -5
View File
@@ -13,12 +13,11 @@
# #
# #
from typing import TYPE_CHECKING
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import SafeString
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic.edit import DeleteView, FormView 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.models import Customer, StudentCard
from counter.utils import is_logged_in_counter from counter.utils import is_logged_in_counter
if TYPE_CHECKING:
from django.utils.safestring import SafeString
class StudentCardDeleteView(DeleteView): class StudentCardDeleteView(DeleteView):
"""View used to delete a card from a user. This is a fragment view !""" """View used to delete a card from a user. This is a fragment view !"""
+1 -1
View File
@@ -217,7 +217,7 @@ C'est une technologie simple et puissante qui se veut comme le jQuery du web mod
### Htmx ### Htmx
[Site officiel](https://four.htmx.org/) [Site officiel](https://htmx.org/)
En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx. En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx.
C'est une librairie js qui s'utilise également au moyen d'attributs HTML à C'est une librairie js qui s'utilise également au moyen d'attributs HTML à
+3 -1
View File
@@ -17,15 +17,17 @@ D'abord, il faut ajouter dans node via `package.json`:
} }
``` ```
Ensuite, pour faire fonctionne l'auto-complétion, il faut configurer `tsconfig.bundled.json`: Ensuite, pour faire fonctionne l'auto-complétion, il faut configurer `tsconfig.json`:
```json ```json
{ {
"compilerOptions": {
// ... // ...
"paths": { "paths": {
// ... // ...
"#mon_app:*": ["./mon_app/static/bundled/*"] "#mon_app:*": ["./mon_app/static/bundled/*"]
} }
}
} }
``` ```
+2 -6
View File
@@ -63,6 +63,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
# liste des autres migrations à appliquer avant celle-ci # liste des autres migrations à appliquer avant celle-ci
] ]
@@ -116,16 +117,14 @@ Par exemple :
```python ```python
from django.db import migrations from django.db import migrations
def forwards_func(apps, schema_editor): def forwards_func(apps, schema_editor):
print("Appplication de la migration") print("Appplication de la migration")
def reverse_func(apps, schema_editor): def reverse_func(apps, schema_editor):
print("Annulation de la migration") print("Annulation de la migration")
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [] dependencies = []
operations = [ operations = [
@@ -165,7 +164,6 @@ On écrirait donc, dans l'application `pedagogy` :
from django.db import models from django.db import models
from core.models import User from core.models import User
class UserUe(models.Model): class UserUe(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
ue = models.CharField(max_length=10) ue = models.CharField(max_length=10)
@@ -176,7 +174,6 @@ Et nous aurions le fichier de migration suivant :
from django.db import migrations, models from django.db import migrations, models
from django.conf import settings from django.conf import settings
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
@@ -292,7 +289,6 @@ La commande vous donnera ceci :
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")] replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]
+5 -1
View File
@@ -191,6 +191,7 @@ for user in richest.annotate(amount=F("customer__amount"))[:100]:
On aurait même pu réorganiser ça : On aurait même pu réorganiser ça :
```python ```python
from core.models import User from core.models import User
from django.db.models import F from django.db.models import F
@@ -238,7 +239,10 @@ nous écrivons donc instinctivement :
from counter.models import Counter from counter.models import Counter
foyer = Counter.objects.get(name="Foyer") foyer = Counter.objects.get(name="Foyer")
total_amount = sum(sale.amount * sale.unit_price for sale in foyer.sellings.all()) total_amount = sum(
sale.amount * sale.unit_price
for sale in foyer.sellings.all()
)
``` ```
On pourrait penser qu'il n'y a pas de problème. On pourrait penser qu'il n'y a pas de problème.
+2 -1
View File
@@ -27,11 +27,12 @@ SITH_SUBSCRIPTIONS = {
"cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6}, "cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666}, "membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333}, "un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
# On rajoute ici notre cotisation # On rajoute ici notre cotisation
# Elle se nomme "Un mois" # Elle se nomme "Un mois"
# Coûte 6€ # Coûte 6€
# Dure 1 mois (on raisonne en semestre, ici, c'est 1/6 de semestre) # Dure 1 mois (on raisonne en semestre, ici, c'est 1/6 de semestre)
"un-mois": {"name": _("One month"), "price": 6, "duration": 0.166}, "un-mois": {"name": _("One month"), "price": 6, "duration": 0.166}
} }
``` ```
+1 -1
View File
@@ -15,7 +15,7 @@ Si le mot est dans le code Python :
```python ```python
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
help_text = _("Hello") help_text=_("Hello")
``` ```
Si le mot apparaît dans le template Jinja : Si le mot apparaît dans le template Jinja :
-1
View File
@@ -26,7 +26,6 @@ from django.templatetags.static import static
# Sélectionnez le fichier de bannière pour le weekmail de l'automne 2018 # Sélectionnez le fichier de bannière pour le weekmail de l'automne 2018
def get_banner(self): def get_banner(self):
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA18.jpg") return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA18.jpg")
``` ```
+2 -3
View File
@@ -147,15 +147,14 @@ Voici quelques exemples :
env = Env() env = Env()
env.read_env() env.read_env()
async def main(): async def main():
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
base_url="https://ae.utbm.fr/api/", headers={"X-APIKey": env.str("API_KEY")} base_url="https://ae.utbm.fr/api/",
headers={"X-APIKey": env.str("API_KEY")}
) as session: ) as session:
async with session.get("club/1") as res: async with session.get("club/1") as res:
print(await res.json()) print(await res.json())
asyncio.run(main()) asyncio.run(main())
``` ```
+8 -4
View File
@@ -182,7 +182,7 @@ qui seront alors injectés.
def get_context_data(**kwargs): def get_context_data(**kwargs):
return super().get_context_data(**kwargs) | { return super().get_context_data(**kwargs) | {
"create_fragment": FooCreateFragment.as_fragment()(), "create_fragment": FooCreateFragment.as_fragment()(),
"update_fragment": FooUpdateFragment.as_fragment()(foo_id=1), "update_fragment": FooUpdateFragment.as_fragment()(foo_id=1)
} }
``` ```
@@ -319,9 +319,11 @@ from core.views.mixins import UseFragmentsMixin
class FooCompositeFormView(UseFragmentsMixin, TemplateView): class FooCompositeFormView(UseFragmentsMixin, TemplateView):
fragments = { fragments = {
"create_fragment": FooCreateFragment, "create_fragment": FooCreateFragment,
"update_fragment": FooUpdateFragment, "update_fragment": FooUpdateFragment
}
fragment_data = {
"update_fragment": {"foo": 4}
} }
fragment_data = {"update_fragment": {"foo": 4}}
template_name = "app/foo.jinja" template_name = "app/foo.jinja"
``` ```
@@ -340,7 +342,9 @@ from core.views.mixins import UseFragmentsMixin
class FooCompositeFormView(UseFragmentsMixin, TemplateView): class FooCompositeFormView(UseFragmentsMixin, TemplateView):
fragments = {"create_fragment": FooCreateFragment} fragments = {
"create_fragment": FooCreateFragment
}
template_name = "app/foo.jinja" template_name = "app/foo.jinja"
def get_fragment_context_data(self): def get_fragment_context_data(self):
-51
View File
@@ -1,51 +0,0 @@
## syntaxe aemark
Le site AE utilise markdown pour le rendu de la plupart
des textes saisis par les utilisateurs.
Cependant, la syntaxe utilisée n'est pas celle officielle
telle que définie par John Gruber,
mais est basée sur [CommonMark](https://commonmark.org/),
avec quelques variations pour les usages particuliers du site AE.
Les deux principaux ajouts sont :
- Les urls commençant par `page://` sont modifiées pour commencer par `/page/`
- Des modificateurs de taille peuvent être indiqués directement dans la source
d'une image
Les variations d'aemark sont documentées sur
[le site AE](https://ae.utbm.fr/page/Aide_sur_la_syntaxe/).
Le code est hébergé sur le dépôt Git [aemark](https://github.com/ae-utbm/ae-markdown).
## Utiliser aemark
Le code du parser aemark est écrit en Rust dans une librairie
indépendante, avec des bindings vers différents langages.
Les librairies mises à disposition exposent une seule fonction,
qui prend simplement du markdown en entrée et renvoie l'HTML correspondant.
Les librairies pour les différents langages sont toutes
basées sur le même code Rust.
La seule différence entre chacune tient uniquement dans la manière
d'intégrer ce code dans les bindings.
De cette manière, le comportement est assuré d'être le même
sur toutes les plateformes disponibles, avec en prime des performances
respectables.
=== ":simple-python: Python"
```python
from aemark import markdown
result = markdown("this is some *markdown text* with __formatting__")
```
=== ":simple-javascript: Javascript"
```typescript
import { markdown } from "@ae_utbm/aemark"
const result = markdown("this is some *markdown text* with __formatting__");
```
+5 -7
View File
@@ -123,7 +123,6 @@ Par exemple, prenons le modèle suivant :
```python ```python
from django.db import models from django.db import models
class News(models.Model): class News(models.Model):
# ... # ...
@@ -200,7 +199,6 @@ Pour les vues sous forme de fonction, il y a le décorateur
from django.views import View from django.views import View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
class NewsModerateView(PermissionRequiredMixin, SingleObjectMixin, View): class NewsModerateView(PermissionRequiredMixin, SingleObjectMixin, View):
model = News model = News
pk_url_kwarg = "news_id" pk_url_kwarg = "news_id"
@@ -227,7 +225,6 @@ Pour les vues sous forme de fonction, il y a le décorateur
from django.urls import reverse from django.urls import reverse
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
@permission_required("com.moderate_news") @permission_required("com.moderate_news")
@require_POST @require_POST
def moderate_news(request, news_id: int): def moderate_news(request, news_id: int):
@@ -266,7 +263,6 @@ from core.auth.mixins import PermissionOrAuthorRequiredMixin
from django.views.generic import UpdateView from django.views.generic import UpdateView
class NewsUpdateView(PermissionOrAuthorRequiredMixin, UpdateView): class NewsUpdateView(PermissionOrAuthorRequiredMixin, UpdateView):
model = News model = News
pk_url_kwarg = "news_id" pk_url_kwarg = "news_id"
@@ -328,8 +324,8 @@ Voici un exemple d'implémentation de ce système :
from core.models import User, Group from core.models import User, Group
class Article(models.Model): class Article(models.Model):
title = models.CharField(_("title"), max_length=100) title = models.CharField(_("title"), max_length=100)
content = models.TextField(_("content")) content = models.TextField(_("content"))
@@ -374,7 +370,6 @@ Voici un exemple d'implémentation de ce système :
from core.models import User, Group from core.models import User, Group
class Article(models.Model): class Article(models.Model):
title = models.CharField(_("title"), max_length=100) title = models.CharField(_("title"), max_length=100)
content = models.TextField(_("content")) content = models.TextField(_("content"))
@@ -534,7 +529,10 @@ class NewsQuerySet(models.QuerySet): # (1)!
return self return self
# sinon, on retourne les nouvelles modérées ou dont l'utilisateur # sinon, on retourne les nouvelles modérées ou dont l'utilisateur
# est l'auteur # est l'auteur
return self.filter(models.Q(is_moderated=True) | models.Q(author=user)) return self.filter(
models.Q(is_moderated=True)
| models.Q(author=user)
)
class News(models.Model): class News(models.Model):
@@ -61,10 +61,10 @@ document.addEventListener("alpine:init", () => {
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
path: { basket_id: basket.id }, path: { basket_id: basket.id },
}); });
if (res.response?.ok) { if (res.response.ok) {
this.data = res.data as Record<string, string>; this.data = res.data as Record<string, string>;
this.isCbAvailable = true; this.isCbAvailable = true;
} else if (res.response?.status === 410) { } else if (res.response.status === 410) {
// The basket is expired, so no payment method should be available at all. // The basket is expired, so no payment method should be available at all.
// This shouldn't happen, because we don't send the request // This shouldn't happen, because we don't send the request
// when the timeout is passed, but we are better safe than sorry // when the timeout is passed, but we are better safe than sorry
@@ -148,6 +148,56 @@
</span> </span>
</div> </div>
{% endif %} {% endif %}
<section>
<div class="category-header">
<h3 class="margin-bottom">{% trans %}Eurockéennes 2025 partnership{% endtrans %}</h3>
{% if user.is_subscribed %}
<div id="eurock-partner" style="
min-height: 600px;
background-color: lightgrey;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 10px;
">
<p style="text-align: center;">
{% trans trimmed %}
Our partner uses Weezevent to sell tickets.
Weezevent may collect user info according to
its own privacy policy.
By clicking the accept button you consent to
their terms of services.
{% endtrans %}
</p>
<a href="https://weezevent.com/fr/politique-de-confidentialite/">{% trans %}Privacy policy{% endtrans %}</a>
<button
hx-get="{{ url("eboutic:eurock") }}"
hx-target="#eurock-partner"
hx-swap="outerHTML"
hx-trigger="click, load[document.cookie.includes('weezevent_accept=true')]"
@htmx:after-request="document.cookie = 'weezevent_accept=true'"
>{% trans %}Accept{% endtrans %}
</button>
</div>
{% else %}
<p>
{%- trans trimmed %}
You must be subscribed to benefit from the partnership with the Eurockéennes.
{% endtrans -%}
</p>
<p>
{%- trans trimmed %}
This partnership offers a discount of up to 33%
on tickets for Friday, Saturday and Sunday,
as well as the 3-day package from Friday to Sunday.
{% endtrans -%}
</p>
{% endif %}
</div>
</section>
{% for prices in categories %} {% for prices in categories %}
{% set category = prices[0].product.product_type %} {% set category = prices[0].product.product_type %}
<section> <section>
@@ -0,0 +1,16 @@
<a title="Logiciel billetterie en ligne"
href="https://www.weezevent.com?c=sys_widget"
class="weezevent-widget-integration"
target="_blank"
data-src="https://widget.weezevent.com/ticket/8aaba226-f7a3-4192-a64e-72ff8f5b35b7?id_evenement=1419869&locale=fr-FR&code=28747"
data-width="650"
data-height="600"
data-resize="1"
data-nopb="0"
data-type="neo"
data-width_auto="1"
data-noscroll="0"
data-id="1419869">
Billetterie Weezevent
</a>
<script type="text/javascript" src="https://widget.weezevent.com/weez.js" async defer></script>
+2 -5
View File
@@ -1,12 +1,13 @@
import re import re
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import TYPE_CHECKING
import freezegun import freezegun
import pytest import pytest
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from django.conf import settings from django.conf import settings
from django.http import HttpResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import Client
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import localdate, now from django.utils.timezone import localdate, now
from model_bakery import baker from model_bakery import baker
@@ -30,10 +31,6 @@ from counter.models import (
from counter.tests.test_counter import BasketItem from counter.tests.test_counter import BasketItem
from eboutic.models import Basket from eboutic.models import Basket
if TYPE_CHECKING:
from django.http import HttpResponse
from django.test.client import Client
@pytest.mark.django_db @pytest.mark.django_db
def test_get_eboutic(): def test_get_eboutic():
+2 -4
View File
@@ -1,6 +1,7 @@
from typing import TYPE_CHECKING, Callable from typing import Callable
import pytest import pytest
from django.test import Client
from django.urls import reverse from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertRedirects from pytest_django.asserts import assertRedirects
@@ -9,9 +10,6 @@ from core.baker_recipes import subscriber_user
from core.models import User from core.models import User
from counter.models import BillingInfo from counter.models import BillingInfo
if TYPE_CHECKING:
from django.test import Client
@pytest.mark.django_db @pytest.mark.django_db
class TestBillingInfo: class TestBillingInfo:

Some files were not shown because too many files have changed in this diff Show More