{% trans %}Bank account: {% endtrans %}{{ object.name }}
- {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
- {% trans %}Delete{% endtrans %}
- {% endif %}
-
{% trans %}Infos{% endtrans %}
-
-
{% trans %}IBAN: {% endtrans %}{{ object.iban }}
-
{% trans %}Number: {% endtrans %}{{ object.number }}
{% trans %}Bank account: {% endtrans %}{{ object.name }}
+ {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
+ {% trans %}Delete{% endtrans %}
+ {% endif %}
+
{% trans %}Infos{% endtrans %}
+
+
{% trans %}IBAN: {% endtrans %}{{ object.iban }}
+
{% trans %}Number: {% endtrans %}{{ object.number }}
{% trans %}Club account:{% endtrans %} {{ object.name }}
- {% if user.is_root and not object.journals.exists() %}
- {% trans %}Delete{% endtrans %}
- {% endif %}
- {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
-
{% trans %}Club account:{% endtrans %} {{ object.name }}
+ {% if user.is_root and not object.journals.exists() %}
+ {% trans %}Delete{% endtrans %}
+ {% endif %}
+ {% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
+
{{ o.remark }}
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
-
- {% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
-
-
- {% trans url=o.target.get_absolute_url() %}Open a journal in this club account, then save this operation again to make the linked operation.{% endtrans %}
-
+
+ {% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
+
+
+ {% trans url=o.target.get_absolute_url() %}Open a journal in this club account, then save this operation again to make the linked operation.{% endtrans %}
+
+ {%
+ if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
+ or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
+ %}
+ {% if not o.journal.closed %}
+ {% trans %}Edit{% endtrans %}
{% endif %}
-
- {%
- if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
- or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
- %}
- {% if not o.journal.closed %}
- {% trans %}Edit{% endtrans %}
- {% endif %}
- {% endif %}
-
- {% endif %}
- {% if club_list %}
+ {% endif %}
+ {% if club_list %}
{% trans %}Club list{% endtrans %}
- {%- for c in club_list.all().order_by('name') if c.parent is none %}
+ {%- for c in club_list.all().order_by('name') if c.parent is none %}
{{ display_club(c) }}
- {%- endfor %}
+ {%- endfor %}
- {% else %}
+ {% else %}
{% trans %}There is no club in this website.{% endtrans %}
- {% endif %}
+ {% endif %}
{% endblock %}
diff --git a/club/templates/club/club_members.jinja b/club/templates/club/club_members.jinja
index 817b0170..a64d43ce 100644
--- a/club/templates/club/club_members.jinja
+++ b/club/templates/club/club_members.jinja
@@ -2,81 +2,81 @@
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
{% block content %}
-
{% trans %}Club members{% endtrans %}
- {% if members %}
+
{% trans %}Club members{% endtrans %}
+ {% if members %}
- {% else %}
+ {% else %}
{% trans %}There are no members in this club.{% endtrans %}
{% endblock %}
diff --git a/club/templates/club/mailing.jinja b/club/templates/club/mailing.jinja
index 8a364295..a7421602 100644
--- a/club/templates/club/mailing.jinja
+++ b/club/templates/club/mailing.jinja
@@ -2,107 +2,107 @@
{% from 'core/macros.jinja' import select_all_checkbox %}
{% block title %}
-{% trans %}Mailing lists{% endtrans %}
+ {% trans %}Mailing lists{% endtrans %}
{% endblock %}
{% block content %}
- {% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}
+ {% trans %}Remember : mailing lists need to be moderated, if your new created list is not shown wait until moderation takes action{% endtrans %}
- {% if mailings_not_moderated %}
-
{% trans %}Mailing lists waiting for moderation{% endtrans %}
+
+
{% for c in club_list.order_by('id') %}
- {% set members = c.members.all() %}
- {% if request.GET['branch'] %}
+ {% set members = c.members.all() %}
+ {% if request.GET['branch'] %}
{% set members = members.filter(user__department=request.GET['branch']) %}
- {% endif %}
-
- {% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
- news__is_moderated=True, news__type__in=["WEEKLY",
- "EVENT"]).order_by('start_date', 'end_date') %}
-
+ {% for d in NewsDate.objects.filter(end_date__gte=timezone.now(),
+ news__is_moderated=True, news__type__in=["WEEKLY",
+ "EVENT"]).order_by('start_date', 'end_date') %}
+
- {# Depends on this package https://github.com/lonaru/easy-markdown-editor #}
-
-
- {# The easymde script can be included twice, it's safe in the code #}
-
-
-
diff --git a/core/templates/core/new_user_email.jinja b/core/templates/core/new_user_email.jinja
index e23c6b16..6dff5b3c 100644
--- a/core/templates/core/new_user_email.jinja
+++ b/core/templates/core/new_user_email.jinja
@@ -1,17 +1,17 @@
{% autoescape off %}
-{% trans %}You're receiving this email because you subscribed to the UTBM student association.{% endtrans %}
+ {% trans %}You're receiving this email because you subscribed to the UTBM student association.{% endtrans %}
-{% trans %}Please go to the following page and choose a new password:{% endtrans %}
-{% block reset_link %}
-{{ protocol }}://{{ domain }}{{ url('core:password_reset_confirm', uidb64=uid, token=token) }}
-{% endblock %}
-{% trans %}Your username, in case it was not given to you: {% endtrans %} {{ user.get_username() }}
-{% trans %}You also got a new account that will be useful to purchase products in the living areas and on the Eboutic.{% endtrans %}
-{% trans account=user.customer.account_id %}Here is your account number: {{ account }}{% endtrans %}
+ {% trans %}Please go to the following page and choose a new password:{% endtrans %}
+ {% block reset_link %}
+ {{ protocol }}://{{ domain }}{{ url('core:password_reset_confirm', uidb64=uid, token=token) }}
+ {% endblock %}
+ {% trans %}Your username, in case it was not given to you: {% endtrans %} {{ user.get_username() }}
+ {% trans %}You also got a new account that will be useful to purchase products in the living areas and on the Eboutic.{% endtrans %}
+ {% trans account=user.customer.account_id %}Here is your account number: {{ account }}{% endtrans %}
-{% trans %}Thanks for subscribing! {% endtrans %}
+ {% trans %}Thanks for subscribing! {% endtrans %}
-{% trans %}The AE team{% endtrans %}
+ {% trans %}The AE team{% endtrans %}
{% endautoescape %}
diff --git a/core/templates/core/new_user_email_subject.jinja b/core/templates/core/new_user_email_subject.jinja
index 103149c4..f992d0a7 100644
--- a/core/templates/core/new_user_email_subject.jinja
+++ b/core/templates/core/new_user_email_subject.jinja
@@ -1,3 +1,3 @@
{% autoescape off %}
-{% trans %}New subscription to the UTBM student association{% endtrans %}
+ {% trans %}New subscription to the UTBM student association{% endtrans %}
{% endautoescape %}
diff --git a/core/templates/core/notification_list.jinja b/core/templates/core/notification_list.jinja
index 412d79f4..3b8f2a6b 100644
--- a/core/templates/core/notification_list.jinja
+++ b/core/templates/core/notification_list.jinja
@@ -1,24 +1,24 @@
{% extends "core/base.jinja" %}
{% block title %}
-{% trans %}Notification list{% endtrans %}
+ {% trans %}Notification list{% endtrans %}
{% endblock %}
{% block content %}
-
{% trans %}Notification list{% endtrans %}
-
-{% for n in notification_list %}
- {% if n.viewed %}
-
{% trans %}You successfully reset your password!{% endtrans %}
+ {% trans %}Login{% endtrans %}
{% endblock %}
diff --git a/core/templates/core/password_reset_confirm.jinja b/core/templates/core/password_reset_confirm.jinja
index e90c9f36..fe172f1f 100644
--- a/core/templates/core/password_reset_confirm.jinja
+++ b/core/templates/core/password_reset_confirm.jinja
@@ -1,14 +1,14 @@
{% extends "core/base.jinja" %}
{% block content %}
-{% if form %}
-
-{% csrf_token %}
-{{ form.as_p() }}
-
-
-{% else %}
-{% trans %}It seems that this link has expired. To generate a new link, you can follow this link: {% endtrans %}{% trans %}lost password{% endtrans %}.
-{% endif %}
+ {% if form %}
+
+ {% csrf_token %}
+ {{ form.as_p() }}
+
+
+ {% else %}
+ {% trans %}It seems that this link has expired. To generate a new link, you can follow this link: {% endtrans %}{% trans %}lost password{% endtrans %}.
+ {% endif %}
{% endblock %}
diff --git a/core/templates/core/password_reset_done.jinja b/core/templates/core/password_reset_done.jinja
index 2f56cac1..f2743e8a 100644
--- a/core/templates/core/password_reset_done.jinja
+++ b/core/templates/core/password_reset_done.jinja
@@ -1,15 +1,15 @@
{% extends "core/base.jinja" %}
{% block content %}
-
{% trans %}Password reset sent{% endtrans %}
+
{% trans %}Password reset sent{% endtrans %}
-
-{% trans %}We've emailed you instructions for setting your password, if an account exists with the email you entered. You should
-receive them shortly.{% endtrans %}
-
+
+ {% trans %}We've emailed you instructions for setting your password, if an account exists with the email you entered. You should
+ receive them shortly.{% endtrans %}
+
-
-{% trans %}If you don't receive an email, please make sure you've entered the address you registered with, and check your spam
-folder.{% endtrans %}
-
+
+ {% trans %}If you don't receive an email, please make sure you've entered the address you registered with, and check your spam
+ folder.{% endtrans %}
+
{% endblock %}
diff --git a/core/templates/core/password_reset_email.jinja b/core/templates/core/password_reset_email.jinja
index 65532865..6e0f6f63 100644
--- a/core/templates/core/password_reset_email.jinja
+++ b/core/templates/core/password_reset_email.jinja
@@ -1,15 +1,15 @@
{% autoescape off %}
-{% trans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endtrans %}
+ {% trans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endtrans %}
-{% trans %}Please go to the following page and choose a new password:{% endtrans %}
-{% block reset_link %}
-{{ protocol }}://{{ domain }}{{ url('core:password_reset_confirm', uidb64=uid, token=token) }}
-{% endblock %}
-{% trans %}Your username, in case you've forgotten: {% endtrans %} {{ user.get_username() }}
+ {% trans %}Please go to the following page and choose a new password:{% endtrans %}
+ {% block reset_link %}
+ {{ protocol }}://{{ domain }}{{ url('core:password_reset_confirm', uidb64=uid, token=token) }}
+ {% endblock %}
+ {% trans %}Your username, in case you've forgotten: {% endtrans %} {{ user.get_username() }}
-{% trans %}Thanks for using our site! {% endtrans %}
+ {% trans %}Thanks for using our site! {% endtrans %}
-{% trans %}The {{ site_name }} team{% endtrans %}
+ {% trans %}The {{ site_name }} team{% endtrans %}
{% endautoescape %}
diff --git a/core/templates/core/poster_list.jinja b/core/templates/core/poster_list.jinja
index 816c8332..fe65658c 100644
--- a/core/templates/core/poster_list.jinja
+++ b/core/templates/core/poster_list.jinja
@@ -1,53 +1,53 @@
{% extends "core/base.jinja" %}
{% block script %}
-{{ super() }}
-
+ {{ super() }}
+
{% endblock %}
{% block title %}
-{% trans %}Poster{% endtrans %}
+ {% trans %}Poster{% endtrans %}
{% endblock %}
{% block content %}
-
{% endblock %}
-
\ No newline at end of file
diff --git a/core/templates/core/register_confirm_mail.jinja b/core/templates/core/register_confirm_mail.jinja
index 9b313035..2bc321b0 100644
--- a/core/templates/core/register_confirm_mail.jinja
+++ b/core/templates/core/register_confirm_mail.jinja
@@ -1,17 +1,17 @@
{% autoescape off %}
-{% trans %}You're receiving this email because you created an account on the AE website.{% endtrans %}
+ {% trans %}You're receiving this email because you created an account on the AE website.{% endtrans %}
-{% trans %}Your username, in case it was not given to you: {% endtrans %} {{ username }}
+ {% trans %}Your username, in case it was not given to you: {% endtrans %} {{ username }}
-{% trans %}
-As this is the website of the students of the AE, by the students of the AE,
-for the students of the AE, you won't be able to do many things without subscribing to the AE.
-To make a contribution, contact a member of the association's board, either directly or by email at ae@utbm.fr.
-{% endtrans %}
+ {% trans %}
+ As this is the website of the students of the AE, by the students of the AE,
+ for the students of the AE, you won't be able to do many things without subscribing to the AE.
+ To make a contribution, contact a member of the association's board, either directly or by email at ae@utbm.fr.
+ {% endtrans %}
-{% trans %}Wishing you a good experience among us! {% endtrans %}
+ {% trans %}Wishing you a good experience among us! {% endtrans %}
-{% trans %}The AE team{% endtrans %}
+ {% trans %}The AE team{% endtrans %}
{% endautoescape %}
diff --git a/core/templates/core/screen_slideshow.jinja b/core/templates/core/screen_slideshow.jinja
index 4dd63884..9e386fcf 100644
--- a/core/templates/core/screen_slideshow.jinja
+++ b/core/templates/core/screen_slideshow.jinja
@@ -1,30 +1,30 @@
-
+
{% trans %}Slideshow{% endtrans %}
-
-
+
+
-
+
{% for poster in posters %}
-
-
-
+
+
+
{% endfor %}
-
+
-
+
{% for poster in posters %}
-
+
{% endfor %}
-
+
-
+
-
+
diff --git a/core/templates/core/search.jinja b/core/templates/core/search.jinja
index bd38874e..f413debb 100644
--- a/core/templates/core/search.jinja
+++ b/core/templates/core/search.jinja
@@ -3,24 +3,24 @@
{% from "core/macros.jinja" import user_link_with_pict %}
{% block title %}
- {% trans %}Search result{% endtrans %}
+ {% trans %}Search result{% endtrans %}
{% endblock %}
{% block content %}
-
+ {% endif %}
+ {% if customer.refillings.exists() %}
+
{% trans %}Reloads{% endtrans %}
+
+
+
+
{% trans %}Date{% endtrans %}
+
{% trans %}Counter{% endtrans %}
+
{% trans %}Barman{% endtrans %}
+
{% trans %}Amount{% endtrans %}
+
{% trans %}Payment method{% endtrans %}
-
-
- {% for i in customer.buyings.order_by('-date').all().filter(
+
+
+ {% for i in customer.refillings.order_by('-date').filter( date__year=year, date__month=month) %}
+
{% if
- user == profile
- or user.memberships.ongoing().exists()
- or user.is_board_member
- or user.is_in_group(name=settings.SITH_BAR_MANAGER_BOARD_GROUP)
+ user == profile
+ or user.memberships.ongoing().exists()
+ or user.is_board_member
+ or user.is_in_group(name=settings.SITH_BAR_MANAGER_BOARD_GROUP)
%}
{# if the user is member of a club, he can view the subscription state #}
-
- {% if profile.is_subscribed %}
- {% if user == profile or user.is_root or user.is_board_member %}
-
- {{ user_subscription(profile) }}
-
- {% endif %}
- {% if user == profile or user.is_root or user.is_board_member or user.is_launderette_manager %}
-
- {# Shows tokens bought by the user #}
- {{ show_tokens(profile) }}
- {# Shows slots took by the user #}
- {{ show_slots(profile) }}
-
- {%- for field in form -%}
+ {%- for field in form -%}
{%-
if field.name in ["quote","profile_pict","avatar_pict","scrub_pict","is_subscriber_viewable","forum_signature"]
-%}
{%- continue -%}
- {%- endif -%}
+ {%- endif -%}
-
-
{{ field.label }}
-
- {{ field }}
- {%- if field.errors -%}
-
{{ field.errors }}
- {%- endif -%}
-
+
+
{{ field.label }}
+
+ {{ field }}
+ {%- if field.errors -%}
+
{{ field.errors }}
+ {%- endif -%}
- {%- endfor -%}
-
+
+{%- endfor -%}
+
{# Textareas #}
-
- {%- for field in [form["quote"], form["forum_signature"]] -%}
-
-
{{ field.label }}
-
- {{ field }}
- {%- if field.errors -%}
-
{{ field.errors }}
- {%- endif -%}
-
-
- {%- endfor -%}
+
+ {%- for field in [form["quote"], form["forum_signature"]] -%}
+
- {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually
- add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}
-
- {% endif %}
+ {% if profile.customer.student_cards.exists() %}
+
+ {% for card in profile.customer.student_cards.all() %}
+
+ {% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually
+ add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}
+
- {% for b in settings.SITH_COUNTER_BARS %}
- {% if user.is_in_group(name=b[1]+" admin") %}
- {% set c = Counter.objects.filter(id=b[0]).first() %}
-
-
+ {% set is_admin_on_a_counter = false %}
+ {% for b in settings.SITH_COUNTER_BARS if user.is_in_group(name=b[1] + " admin") %}
+ {% set is_admin_on_a_counter = true %}
+ {% endfor %}
+
+ {% if
+ is_admin_on_a_counter
+ or user.is_root
+ or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
+ %}
+
+
{% trans %}Counters{% endtrans %}
+
+ {% if user.is_root
+ or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
+ %}
+
+ {% for b in settings.SITH_COUNTER_BARS %}
+ {% if user.is_in_group(name=b[1]+" admin") %}
+ {% set c = Counter.objects.filter(id=b[0]).first() %}
+
+
{% endblock %}
\ No newline at end of file
diff --git a/core/templates/core/widgets/markdown_textarea.jinja b/core/templates/core/widgets/markdown_textarea.jinja
new file mode 100644
index 00000000..a97dc80f
--- /dev/null
+++ b/core/templates/core/widgets/markdown_textarea.jinja
@@ -0,0 +1,198 @@
+
+ {# Depends on this package https://github.com/lonaru/easy-markdown-editor #}
+
+
+ {# The easymde script can be included twice, it's safe in the code #}
+
+
+
diff --git a/core/templates/core/widgets/nfc.jinja b/core/templates/core/widgets/nfc.jinja
new file mode 100644
index 00000000..04dc288d
--- /dev/null
+++ b/core/templates/core/widgets/nfc.jinja
@@ -0,0 +1,33 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/templatetags/renderer.py b/core/templatetags/renderer.py
index 8d6bac64..cfd3ef91 100644
--- a/core/templatetags/renderer.py
+++ b/core/templatetags/renderer.py
@@ -23,15 +23,17 @@
#
import datetime
+from pathlib import Path
import phonenumbers
from django import template
+from django.template import TemplateSyntaxError
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
from core.markdown import markdown as md
-from core.scss.processor import ScssProcessor
+from core.scss.processor import process_scss_path
register = template.Library()
@@ -86,5 +88,7 @@ def format_timedelta(value: datetime.timedelta) -> str:
@register.simple_tag()
def scss(path):
"""Return path of the corresponding css file after compilation."""
- processor = ScssProcessor(path)
- return processor.get_converted_scss()
+ path = Path(path)
+ if path.suffix != ".scss":
+ raise TemplateSyntaxError("`scss` tag has been called with a non-scss file")
+ return process_scss_path(path)
diff --git a/core/tests.py b/core/tests.py
index 6fa24874..1377a5ae 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -14,7 +14,6 @@
#
from datetime import date, timedelta
-from pathlib import Path
from smtplib import SMTPException
import freezegun
@@ -210,7 +209,7 @@ def test_custom_markdown_syntax(md, html):
def test_full_markdown_syntax():
- syntax_path = Path(settings.BASE_DIR) / "core" / "fixtures"
+ syntax_path = settings.BASE_DIR / "core" / "fixtures"
md = (syntax_path / "SYNTAX.md").read_text()
html = (syntax_path / "SYNTAX.html").read_text()
result = markdown(md)
diff --git a/core/utils.py b/core/utils.py
index 41d132bc..b336a125 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -13,7 +13,6 @@
#
#
-import os
import re
import subprocess
from datetime import date
@@ -96,10 +95,6 @@ def get_semester_code(d: Optional[date] = None) -> str:
return "P" + str(start.year)[-2:]
-def file_exist(path):
- return os.path.exists(path)
-
-
def scale_dimension(width, height, long_edge):
if width > height:
ratio = long_edge * 1.0 / width
diff --git a/core/views/files.py b/core/views/files.py
index 83f7e00c..161578d0 100644
--- a/core/views/files.py
+++ b/core/views/files.py
@@ -14,7 +14,6 @@
#
# This file contains all the views that concern the page model
-import os
from wsgiref.util import FileWrapper
from ajax_select import make_ajax_field
@@ -59,17 +58,17 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
):
raise PermissionDenied
name = f.__getattribute__(file_attr).name
- filepath = os.path.join(settings.MEDIA_ROOT, name)
+ filepath = settings.MEDIA_ROOT / name
# check if file exists on disk
- if not os.path.exists(filepath.encode("utf-8")):
- raise Http404()
+ if not filepath.exists():
+ raise Http404
- with open(filepath.encode("utf-8"), "rb") as filename:
+ with open(filepath, "rb") as filename:
wrapper = FileWrapper(filename)
response = HttpResponse(wrapper, content_type=f.mime_type)
response["Last-Modified"] = http_date(f.date.timestamp())
- response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
+ response["Content-Length"] = filepath.stat().st_size
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
"utf-8"
)
diff --git a/core/views/forms.py b/core/views/forms.py
index ff649848..93feffe9 100644
--- a/core/views/forms.py
+++ b/core/views/forms.py
@@ -20,7 +20,6 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
-import datetime
import re
from io import BytesIO
@@ -39,14 +38,11 @@ from django.forms import (
Textarea,
TextInput,
)
-from django.forms.utils import to_current_timezone
from django.templatetags.static import static
from django.urls import reverse
-from django.utils import timezone
-from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
-from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
+from phonenumber_field.widgets import RegionalPhoneNumberWidget
from PIL import Image
from core.models import Gift, Page, SithFile, User
@@ -56,25 +52,21 @@ from core.utils import resize_image
class SelectDateTime(DateTimeInput):
- def render(self, name, value, attrs=None, renderer=None):
- if attrs:
- attrs["class"] = "select_datetime"
- else:
- attrs = {"class": "select_datetime"}
- return super().render(name, value, attrs, renderer)
+ def __init__(self, attrs=None, format=None): # noqa A002
+ default = {"type": "datetime-local"}
+ attrs = default if attrs is None else default | attrs
+ super().__init__(attrs=attrs, format=format or "%Y-%m-%d %H:%M")
class SelectDate(DateInput):
- def render(self, name, value, attrs=None, renderer=None):
- if attrs:
- attrs["class"] = "select_date"
- else:
- attrs = {"class": "select_date"}
- return super().render(name, value, attrs, renderer)
+ def __init__(self, attrs=None, format=None): # noqa A002
+ default = {"type": "date"}
+ attrs = default if attrs is None else default | attrs
+ super().__init__(attrs=attrs, format=format or "%Y-%m-%d")
class MarkdownInput(Textarea):
- template_name = "core/markdown_textarea.jinja"
+ template_name = "core/widgets/markdown_textarea.jinja"
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
@@ -108,6 +100,15 @@ class MarkdownInput(Textarea):
return context
+class NFCTextInput(TextInput):
+ template_name = "core/widgets/nfc.jinja"
+
+ def get_context(self, name, value, attrs):
+ context = super().get_context(name, value, attrs)
+ context["translations"] = {"unsupported": _("Unsupported NFC card")}
+ return context
+
+
class SelectFile(TextInput):
def render(self, name, value, attrs=None, renderer=None):
if attrs:
@@ -235,8 +236,8 @@ class UserProfileForm(forms.ModelForm):
"profile_pict": forms.ClearableFileInput,
"avatar_pict": forms.ClearableFileInput,
"scrub_pict": forms.ClearableFileInput,
- "phone": PhoneNumberInternationalFallbackWidget,
- "parent_phone": PhoneNumberInternationalFallbackWidget,
+ "phone": RegionalPhoneNumberWidget,
+ "parent_phone": RegionalPhoneNumberWidget,
"quote": forms.Textarea,
}
labels = {
@@ -247,12 +248,6 @@ class UserProfileForm(forms.ModelForm):
"scrub_pict": _("Scrub: let other know how your scrub looks like!"),
}
- def __init__(self, *arg, **kwargs):
- super().__init__(*arg, **kwargs)
-
- def full_clean(self):
- super().full_clean()
-
def generate_name(self, field_name, f):
field_name = field_name[:-4]
return field_name + str(self.instance.id) + "." + f.content_type.split("/")[-1]
@@ -394,27 +389,3 @@ class GiftForm(forms.ModelForm):
id=user_id
)
self.fields["user"].widget = forms.HiddenInput()
-
-
-class TzAwareDateTimeField(forms.DateTimeField):
- def __init__(self, input_formats=None, widget=SelectDateTime, **kwargs):
- if input_formats is None:
- input_formats = ["%Y-%m-%d %H:%M:%S"]
- super().__init__(input_formats=input_formats, widget=widget, **kwargs)
-
- def prepare_value(self, value):
- # the db value is a datetime as a string in UTC
- if isinstance(value, str):
- # convert it into a naive datetime (no timezone attached)
- value = parse_datetime(value)
- # attach it to the UTC timezone (so that to_current_timezone()) if not None
- # converts it to the local timezone)
- if value is not None:
- value = timezone.make_aware(value, datetime.timezone.utc)
-
- if isinstance(value, datetime.datetime):
- value = to_current_timezone(value)
- # otherwise it is formatted according to locale (in french)
- value = str(value)
-
- return value
diff --git a/core/views/user.py b/core/views/user.py
index 4dfce360..86043f9f 100644
--- a/core/views/user.py
+++ b/core/views/user.py
@@ -21,6 +21,7 @@
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
+import itertools
# This file contains all the views that concern the user model
from datetime import date, timedelta
@@ -31,6 +32,7 @@ from django.contrib.auth import login, views
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError
+from django.db.models import F
from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse
@@ -311,21 +313,15 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
- kwargs["albums"] = []
- kwargs["pictures"] = {}
- picture_qs = (
+ pictures = list(
Picture.objects.filter(people__user_id=self.object.id)
- .order_by("parent__date", "id")
- .all()
+ .order_by("-parent__date", "-date")
+ .annotate(album=F("parent__name"))
)
- last_album = None
- for picture in picture_qs:
- album = picture.parent
- if album.id != last_album and album not in kwargs["albums"]:
- kwargs["albums"].append(album)
- kwargs["pictures"][album.id] = []
- last_album = album.id
- kwargs["pictures"][album.id].append(picture)
+ kwargs["albums"] = {
+ album: list(picts)
+ for album, picts in itertools.groupby(pictures, lambda i: i.album)
+ }
return kwargs
diff --git a/counter/forms.py b/counter/forms.py
index 4b5579b7..734cfcf6 100644
--- a/counter/forms.py
+++ b/counter/forms.py
@@ -3,7 +3,7 @@ from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultip
from django import forms
from django.utils.translation import gettext_lazy as _
-from core.views.forms import SelectDate, TzAwareDateTimeField
+from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
from counter.models import (
BillingInfo,
Counter,
@@ -37,6 +37,9 @@ class StudentCardForm(forms.ModelForm):
class Meta:
model = StudentCard
fields = ["uid"]
+ widgets = {
+ "uid": NFCTextInput,
+ }
def clean(self):
cleaned_data = super().clean()
@@ -55,7 +58,10 @@ class GetUserForm(forms.Form):
"""
code = forms.CharField(
- label="Code", max_length=StudentCard.UID_SIZE, required=False
+ label="Code",
+ max_length=StudentCard.UID_SIZE,
+ required=False,
+ widget=NFCTextInput,
)
id = AutoCompleteSelectField(
"users", required=False, label=_("Select user"), help_text=None
@@ -86,6 +92,14 @@ class GetUserForm(forms.Form):
return cleaned_data
+class NFCCardForm(forms.Form):
+ student_card_uid = forms.CharField(
+ max_length=StudentCard.UID_SIZE,
+ required=False,
+ widget=NFCTextInput,
+ )
+
+
class RefillForm(forms.ModelForm):
error_css_class = "error"
required_css_class = "required"
@@ -168,8 +182,12 @@ class ProductEditForm(forms.ModelForm):
class CashSummaryFormBase(forms.Form):
- begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
- end_date = TzAwareDateTimeField(label=_("End date"), required=False)
+ begin_date = forms.DateTimeField(
+ label=_("Begin date"), widget=SelectDateTime, required=False
+ )
+ end_date = forms.DateTimeField(
+ label=_("End date"), widget=SelectDateTime, required=False
+ )
class EticketForm(forms.ModelForm):
diff --git a/counter/templates/counter/activity.jinja b/counter/templates/counter/activity.jinja
index 3ea720d0..4622b7a9 100644
--- a/counter/templates/counter/activity.jinja
+++ b/counter/templates/counter/activity.jinja
@@ -2,43 +2,43 @@
{% from 'core/macros.jinja' import user_profile_link %}
{% block title %}
-{% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
+ {% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
{% endblock %}
{%- block additional_css -%}
-
+
{%- endblock -%}
{% block content %}
-
{% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
- {% if counter.type == 'BAR' %}
-
{% trans %}Barmen list{% endtrans %}
-
- {% if counter.barmen_list | length > 0 %}
- {% for b in counter.barmen_list %}
-
{{ user_profile_link(b) }}
- {% endfor %}
- {% else %}
- {% trans %}There is currently no barman connected.{% endtrans %}
- {% endif %}
-
- {% endif %}
+
{% trans counter_name=counter %}{{ counter_name }} activity{% endtrans %}
+ {% if counter.type == 'BAR' %}
+
{% trans %}Barmen list{% endtrans %}
+
+ {% if counter.barmen_list | length > 0 %}
+ {% for b in counter.barmen_list %}
+
{{ user_profile_link(b) }}
+ {% endfor %}
+ {% else %}
+ {% trans %}There is currently no barman connected.{% endtrans %}
+ {% endif %}
+
+ {% endif %}
-
{% trans %}Legend{% endtrans %}
-
-
-
- {% trans %}counter is open, there's at least one barman connected{% endtrans %}
-
-
-
- {% trans minutes=settings.SITH_COUNTER_MINUTE_INACTIVE %}counter is open but not active, the last sale was done at least {{ minutes }} minutes ago {% endtrans %}
-
-
-
- {% trans %}counter is not open : no one is connected{% endtrans %}
-
+
{% trans %}Legend{% endtrans %}
+
+
+
+ {% trans %}counter is open, there's at least one barman connected{% endtrans %}
+
+
+ {% trans minutes=settings.SITH_COUNTER_MINUTE_INACTIVE %}counter is open but not active, the last sale was done at least {{ minutes }} minutes ago {% endtrans %}
+
+
+
+ {% trans %}counter is not open : no one is connected{% endtrans %}
+
+ {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
-
- {% csrf_token %}
-
-
-
-
-
+
+ {% csrf_token %}
+
+
+
+
+
-
-
-
-
-
{% trans %}Basket: {% endtrans %}
+
+
+
+
+
{% trans %}Basket: {% endtrans %}
-
-
-
-
- {% csrf_token %}
-
-
-
-
+
+
+
+
+ {% csrf_token %}
+
+
+
+
-
+
-
- {% csrf_token %}
-
-
-
-
+
+ {% csrf_token %}
+
+
+
+
- :
-
- €
- P
-
-
-
-
- Total:
-
- €
-
-
-
- {% csrf_token %}
-
-
-
-
- {% csrf_token %}
-
-
-
+ :
+
+ €
+ P
- {% if (counter.type == 'BAR' and barmens_can_refill) %}
-
{% endblock %}
diff --git a/counter/views.py b/counter/views.py
index ed060426..187d0c4a 100644
--- a/counter/views.py
+++ b/counter/views.py
@@ -61,6 +61,7 @@ from counter.forms import (
CounterEditForm,
EticketForm,
GetUserForm,
+ NFCCardForm,
ProductEditForm,
RefillForm,
StudentCardForm,
@@ -679,6 +680,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
)
kwargs["customer"] = self.customer
kwargs["student_cards"] = self.customer.student_cards.all()
+ kwargs["student_card_input"] = NFCCardForm()
kwargs["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm()
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
diff --git a/docs/explanation/conventions.md b/docs/explanation/conventions.md
index 4c7c4930..47989f7e 100644
--- a/docs/explanation/conventions.md
+++ b/docs/explanation/conventions.md
@@ -148,12 +148,14 @@ Ces règles sont automatiquement appliquées quand
vous faites tourner Ruff, donc vous n'avez pas à trop
vous poser de questions de ce côté-là.
-En ce qui concerne les autres langages utilisés
-(Jinja, SCSS, Javascript), nous n'avons pas fixé
-de convention à suivre.
-Pour SCSS et Javascript, vous pouvez utiliser
+En ce qui concerne les templates Jinja
+et les fichiers SCSS, la norme de formatage
+est celle par défaut de `djHTML`.
+
+Pour Javascript, vous pouvez utiliser
Prettier, avec sa configuration par défaut,
-qui est plutôt bonne.
+qui est plutôt bonne,
+mais nous n'avons pas de norme établie pour le projet.
### Qualité du code
diff --git a/docs/explanation/technos.md b/docs/explanation/technos.md
index 29b6e0c5..9d434670 100644
--- a/docs/explanation/technos.md
+++ b/docs/explanation/technos.md
@@ -352,4 +352,17 @@ et les corrige automatiquement (quand c'est possible)
sans que l'utilisateur ait à s'en soucier.
Bien installé, il peut effectuer ce travail
à chaque sauvegarde d'un fichier dans son éditeur,
-ce qui est très agréable pour travailler.
\ No newline at end of file
+ce qui est très agréable pour travailler.
+
+### DjHTML
+
+[Site officiel](https://github.com/rtts/djhtml)
+
+Ruff permet de formater les fichiers Python,
+mais il ne formatte pas les templates et les feuilles de style.
+Pour ça, il faut un autre outil, aisément intégrable
+dans la CI : `djHTML`.
+
+En utilisant conjointement Ruff et djHTML,
+on arrive donc à la fois à formater les fichiers
+Python et les fichiers relatifs au frontend.
diff --git a/eboutic/templates/eboutic/eboutic_main.jinja b/eboutic/templates/eboutic/eboutic_main.jinja
index 9a8470d5..3e1a2239 100644
--- a/eboutic/templates/eboutic/eboutic_main.jinja
+++ b/eboutic/templates/eboutic/eboutic_main.jinja
@@ -1,7 +1,7 @@
{% extends "core/base.jinja" %}
{% block title %}
- {% trans %}Eboutic{% endtrans %}
+ {% trans %}Eboutic{% endtrans %}
{% endblock %}
{% block jquery_css %}
@@ -11,120 +11,120 @@
{% block additional_js %}
{# This script contains the code to perform requests to manipulate the
user basket without having to reload the page #}
-
-
+
+
{% endblock %}
{% block additional_css %}
-
+
{% endblock %}
{% block content %}
-
{% trans %}Eboutic{% endtrans %}
-
-
-
Panier
- {% if errors %}
-
-
- {% for error in errors %}
-
{{ error }}
- {% endfor %}
- {% trans %}Your basket has been cleaned accordingly to those errors.{% endtrans %}
-
+ {% endfor %}
+ {% trans %}Your basket has been cleaned accordingly to those errors.{% endtrans %}
+
-
- {% if not request.user.date_of_birth %}
-
-
- {% trans %}You have not filled in your date of birth. As a result, you may not have access to all the products in the online shop. To fill in your date of birth, you can go to{% endtrans %}
-
- {% trans %}this page{% endtrans %}
-
-
-
-
-
-
+
+ {% trans %}You have not filled in your date of birth. As a result, you may not have access to all the products in the online shop. To fill in your date of birth, you can go to{% endtrans %}
+
+ {% trans %}this page{% endtrans %}
+
+
+
+
+
+
+ {% endif %}
- {% for priority_groups in products|groupby('priority')|reverse %}
- {% for category, items in priority_groups.list|groupby('category') %}
- {% if items|count > 0 %}
-
+ {% for priority_groups in products|groupby('priority')|reverse %}
+ {% for category, items in priority_groups.list|groupby('category') %}
+ {% if items|count > 0 %}
+
{# I would have wholeheartedly directly used the header element instead
but it has already been made messy in core/style.scss #}
-
-
{{ category }}
- {% if items[0].category_comment %}
-
{{ items[0].category_comment }}
- {% endif %}
-
-
- {% for p in items %}
-
- {% endfor %}
-
-
+
+
{{ category }}
+ {% if items[0].category_comment %}
+
{{ items[0].category_comment }}
+ {% endif %}
+
+
+ {% for p in items %}
+
{% endfor %}
- {% else %}
-
{% trans %}There are no items available for sale{% endtrans %}
- {% endfor %}
-
+
+
+ {% endif %}
+ {% endfor %}
+ {% else %}
+
{% trans %}There are no items available for sale{% endtrans %}
- {%- if election.is_vote_active %}
- {% trans %}Polls close {% endtrans %}
- {%- elif election.is_vote_finished %}
- {% trans %}Polls closed {% endtrans %}
- {%- else %}
- {% trans %}Polls will open {% endtrans %}
-
- {% trans %} at {% endtrans %}
- {% trans %}and will close {% endtrans %}
- {%- endif %}
-
- {% trans %} at {% endtrans %}
-
- {%- if election.has_voted(user) %}
-
- {%- if election.is_vote_active %}
- {% trans %}You already have submitted your vote.{% endtrans %}
- {%- else %}
- {% trans %}You have voted in this election.{% endtrans %}
- {%- endif %}
-
+
{{ election.title }}
+
{{ election.description }}
+
+
+
+ {%- if election.is_vote_active %}
+ {% trans %}Polls close {% endtrans %}
+ {%- elif election.is_vote_finished %}
+ {% trans %}Polls closed {% endtrans %}
+ {%- else %}
+ {% trans %}Polls will open {% endtrans %}
+
+ {% trans %} at {% endtrans %}
+ {% trans %}and will close {% endtrans %}
+ {%- endif %}
+
+ {% trans %} at {% endtrans %}
+
+ {%- if election.has_voted(user) %}
+
+ {%- if election.is_vote_active %}
+ {% trans %}You already have submitted your vote.{% endtrans %}
+ {%- else %}
+ {% trans %}You have voted in this election.{% endtrans %}
{%- endif %}
-
-
-
- {% csrf_token %}
-
- {%- set election_lists = election.election_lists.all() -%}
-
-
-
-
{% trans %}Blank vote{% endtrans %}
- {%- for election_list in election_lists %}
-
- {{ election_list.title }}
- {% if user.can_edit(election_list) and election.is_vote_editable -%}
- ❌
- {% endif %}
-
- {%- endfor %}
-
-
- {%- set role_list = election.roles.order_by('order').all() %}
- {%- for role in role_list %}
- {%- set count = [0] %}
- {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %}
-
-
-
-
-
{{ role.title }}
-
{{ role.description }}
- {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %}
- {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %}
- {%- endif %}
+
+ {%- endif %}
+
+
+
+ {% csrf_token %}
+
+ {%- set election_lists = election.election_lists.all() -%}
+
+
+
+
{% trans %}Blank vote{% endtrans %}
+ {%- for election_list in election_lists %}
+
+ {{ election_list.title }}
+ {% if user.can_edit(election_list) and election.is_vote_editable -%}
+ ❌
+ {% endif %}
+
+ {%- endfor %}
+
+
+ {%- set role_list = election.roles.order_by('order').all() %}
+ {%- for role in role_list %}
+ {%- set count = [0] %}
+ {%- set role_data = election_form.data.getlist(role.title) if role.title in election_form.data else [] %}
+
+
+
+
+
{{ role.title }}
+
{{ role.description }}
+ {%- if role.max_choice > 1 and not election.has_voted(user) and election.can_vote(user) %}
+ {% trans %}You may choose up to{% endtrans %} {{ role.max_choice }} {% trans %}people.{% endtrans %}
+ {%- endif %}
- {%- if election_form.errors[role.title] is defined %}
- {%- for error in election_form.errors.as_data()[role.title] %}
- {{ error.message }}
- {%- endfor %}
- {%- endif %}
-
- {% if user.can_edit(role) and election.is_vote_editable -%}
-
+ {%- if election_form.errors[role.title] is defined %}
+ {%- for error in election_form.errors.as_data()[role.title] %}
+ {{ error.message }}
+ {%- endfor %}
+ {%- endif %}
+
+ {% if user.can_edit(role) and election.is_vote_editable -%}
+
{% if
- user.is_com_admin
- or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID)
+ user.is_com_admin
+ or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID)
%}
-
+
{# Do not vote button #}
-
+
{# Star widget #}
- {% for i in number_of_stars %}
+ {% for i in number_of_stars %}
- {% endfor %}
+ {% endfor %}
{# Restaure previous (-1 is default) #}
-
+
\ No newline at end of file
diff --git a/pedagogy/templates/pedagogy/uv_detail.jinja b/pedagogy/templates/pedagogy/uv_detail.jinja
index de741b96..bf45da0f 100644
--- a/pedagogy/templates/pedagogy/uv_detail.jinja
+++ b/pedagogy/templates/pedagogy/uv_detail.jinja
@@ -3,223 +3,223 @@
{% from "pedagogy/macros.jinja" import display_star %}
{% block title %}
-{% trans %}UV Details{% endtrans %}
+ {% trans %}UV Details{% endtrans %}
{% endblock %}
{% block content %}
-
{% csrf_token %}
{{ form.non_field_errors() }}
{% for field in form %}
- {% if field.is_hidden %}
+ {% if field.is_hidden %}
{{ field }}
- {% else %}
-
{% trans %}Delete all forum messages from an user{% endtrans %}
-
- {% csrf_token %}
- {{ form.as_p() }}
-
-
-
{% trans %}If you have trouble using this utility (timeout error, 500 error), try using the command line utility. Use ./manage.py delete_all_forum_user_messages ID.{% endtrans %}
+
{% trans %}Delete all forum messages from an user{% endtrans %}
+
+ {% csrf_token %}
+ {{ form.as_p() }}
+
+
+
{% trans %}If you have trouble using this utility (timeout error, 500 error), try using the command line utility. Use ./manage.py delete_all_forum_user_messages ID.{% endtrans %}
{% endblock %}
\ No newline at end of file
diff --git a/rootplace/templates/rootplace/logs.jinja b/rootplace/templates/rootplace/logs.jinja
index 740ec74f..10155cf1 100644
--- a/rootplace/templates/rootplace/logs.jinja
+++ b/rootplace/templates/rootplace/logs.jinja
@@ -2,31 +2,31 @@
{% from 'core/macros.jinja' import paginate %}
{% block title %}
-{% trans %}Operation logs{% endtrans %}
+ {% trans %}Operation logs{% endtrans %}
{% endblock %}
{% block content %}
-
-
-
-
{% trans %}Date{% endtrans %}
-
{% trans %}Operation type{% endtrans %}
-
{% trans %}Label{% endtrans %}
-
{% trans %}Operator{% endtrans %}
-
-
-
- {% for log in object_list %}
-
-
{{ log.date }}
-
{{ log.get_operation_type_display() }}
-
{{ log.label }}
-
{{ log.operator }}
-
- {% endfor %}
-
-
+
+
+
+
{% trans %}Date{% endtrans %}
+
{% trans %}Operation type{% endtrans %}
+
{% trans %}Label{% endtrans %}
+
{% trans %}Operator{% endtrans %}
+
+
+
+ {% for log in object_list %}
+
+
{{ log.date }}
+
{{ log.get_operation_type_display() }}
+
{{ log.label }}
+
{{ log.operator }}
+
+ {% endfor %}
+
+
-
- {{ paginate(page_obj, paginator) }}
+
+ {{ paginate(page_obj, paginator) }}
{% endblock content %}
\ No newline at end of file
diff --git a/rootplace/templates/rootplace/merge.jinja b/rootplace/templates/rootplace/merge.jinja
index 23084e73..ca7344a4 100644
--- a/rootplace/templates/rootplace/merge.jinja
+++ b/rootplace/templates/rootplace/merge.jinja
@@ -1,14 +1,14 @@
{% extends "core/base.jinja" %}
{% block title %}
-{% trans %}Merge users{% endtrans %}
+ {% trans %}Merge users{% endtrans %}
{% endblock %}
{% block content %}
-
{% trans %}Merge two users{% endtrans %}
-
- {% csrf_token %}
- {{ form.as_p() }}
-
-
+
{% trans %}Merge two users{% endtrans %}
+
+ {% csrf_token %}
+ {{ form.as_p() }}
+
+
{% endblock %}
diff --git a/sas/api.py b/sas/api.py
index c1159df7..ea4c1c74 100644
--- a/sas/api.py
+++ b/sas/api.py
@@ -1,17 +1,19 @@
+from django.conf import settings
from ninja import Query
from ninja_extra import ControllerBase, api_controller, route
from ninja_extra.exceptions import PermissionDenied
from ninja_extra.permissions import IsAuthenticated
+from pydantic import NonNegativeInt
from core.models import User
-from sas.models import Picture
+from sas.models import PeoplePictureRelation, Picture
from sas.schemas import PictureFilterSchema, PictureSchema
-@api_controller("/sas")
-class SasController(ControllerBase):
+@api_controller("/sas/picture")
+class PicturesController(ControllerBase):
@route.get(
- "/picture",
+ "",
response=list[PictureSchema],
permissions=[IsAuthenticated],
url_name="pictures",
@@ -22,11 +24,17 @@ class SasController(ControllerBase):
A user with an active subscription can see any picture, as long
as it has been moderated and not asked for removal.
An unsubscribed user can see the pictures he has been identified on
- (only the moderated ones, too)
+ (only the moderated ones, too).
Notes:
Trying to fetch the pictures of another user with this route
while being unsubscribed will just result in an empty response.
+
+ Notes:
+ Unsubscribed users who are identified is not a rare case.
+ They can be UTT students, faluchards from other schools,
+ or even Richard Stallman (that ain't no joke,
+ cf. https://ae.utbm.fr/user/32663/pictures/)
"""
user: User = self.context.request.user
if not user.is_subscribed and filters.users_identified != {user.id}:
@@ -45,3 +53,23 @@ class SasController(ControllerBase):
picture.compressed_url = picture.get_download_compressed_url()
picture.thumb_url = picture.get_download_thumb_url()
return pictures
+
+
+@api_controller("/sas/relation", tags="User identification on SAS pictures")
+class UsersIdentifiedController(ControllerBase):
+ @route.delete("/{relation_id}", permissions=[IsAuthenticated])
+ def delete_relation(self, relation_id: NonNegativeInt):
+ """Untag a user from a SAS picture.
+
+ Root and SAS admins can delete any picture identification.
+ All other users can delete their own identification.
+ """
+ relation = self.get_object_or_exception(PeoplePictureRelation, pk=relation_id)
+ user: User = self.context.request.user
+ if (
+ relation.user_id != user.id
+ and not user.is_root
+ and not user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
+ ):
+ raise PermissionDenied
+ relation.delete()
diff --git a/sas/models.py b/sas/models.py
index 7c1ae6e8..7330d823 100644
--- a/sas/models.py
+++ b/sas/models.py
@@ -13,7 +13,6 @@
#
#
-import os
from io import BytesIO
from django.conf import settings
@@ -46,9 +45,7 @@ class Picture(SithFile):
@property
def is_vertical(self):
- with open(
- os.path.join(settings.MEDIA_ROOT, self.file.name).encode("utf-8"), "rb"
- ) as f:
+ with open(settings.MEDIA_ROOT / self.file.name, "rb") as f:
im = Image.open(BytesIO(f.read()))
(w, h) = im.size
return (w / h) < 1
@@ -112,9 +109,7 @@ class Picture(SithFile):
def rotate(self, degree):
for attr in ["file", "compressed", "thumbnail"]:
name = self.__getattribute__(attr).name
- with open(
- os.path.join(settings.MEDIA_ROOT, name).encode("utf-8"), "r+b"
- ) as file:
+ with open(settings.MEDIA_ROOT / name, "r+b") as file:
if file:
im = Image.open(BytesIO(file.read()))
file.seek(0)
diff --git a/sas/schemas.py b/sas/schemas.py
index a8e74d20..14388a2d 100644
--- a/sas/schemas.py
+++ b/sas/schemas.py
@@ -1,10 +1,10 @@
from datetime import datetime
-from ninja import FilterSchema, ModelSchema
-from pydantic import Field
+from ninja import FilterSchema, ModelSchema, Schema
+from pydantic import Field, NonNegativeInt
from core.schemas import SimpleUserSchema
-from sas.models import Picture
+from sas.models import PeoplePictureRelation, Picture
class PictureFilterSchema(FilterSchema):
@@ -23,3 +23,14 @@ class PictureSchema(ModelSchema):
full_size_url: str
compressed_url: str
thumb_url: str
+
+
+class PictureCreateRelationSchema(Schema):
+ user_id: NonNegativeInt
+ picture_id: NonNegativeInt
+
+
+class CreatedPictureRelationSchema(ModelSchema):
+ class Meta:
+ model = PeoplePictureRelation
+ fields = ["id", "user", "picture"]
diff --git a/sas/templates/sas/album.jinja b/sas/templates/sas/album.jinja
index 46eba055..60f9dff9 100644
--- a/sas/templates/sas/album.jinja
+++ b/sas/templates/sas/album.jinja
@@ -2,247 +2,247 @@
{% from "core/macros.jinja" import paginate %}
{%- block additional_css -%}
-
+
{%- endblock -%}
{% block title %}
- {% trans %}SAS{% endtrans %}
+ {% trans %}SAS{% endtrans %}
{% endblock %}
{% macro print_path(file) %}
- {% if file and file.parent %}
- {{ print_path(file.parent) }}
- {{ file.get_display_name() }} /
- {% endif %}
+ {% if file and file.parent %}
+ {{ print_path(file.parent) }}
+ {{ file.get_display_name() }} /
+ {% endif %}
{% endmacro %}
{% block content %}
-
- SAS / {{ print_path(album.parent) }} {{ album.get_display_name() }}
-
+
+ SAS / {{ print_path(album.parent) }} {{ album.get_display_name() }}
+
- {% set edit_mode = user.can_edit(album) %}
- {% set start = timezone.now() %}
+ {% set edit_mode = user.can_edit(album) %}
+ {% set start = timezone.now() %}
- {% if edit_mode %}
-
- {% else %}
- {% trans %}This album does not contain any photos.{% endtrans %}
- {% endif %}
-
- {% if pictures.has_previous() or pictures.has_next() %}
-
+ {% else %}
+ {% trans %}This album does not contain any photos.{% endtrans %}
+ {% endif %}
-
+ {% if pictures.has_previous() or pictures.has_next() %}
+
+ {{ paginate(pictures, paginator) }}
+
+ {% endif %}
-
{% trans %}Template generation time: {% endtrans %}
+ {% if edit_mode %}
+
+ {% endif %}
+
+ {% if user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
+
+
- {% if not picture.is_moderated %}
- {% set next = picture.get_next() %}
- {% if not next %}
- {% set next = url('sas:moderation') %}
- {% else %}
- {% set next = next.get_absolute_url() + "#pict" %}
- {% endif %}
-
-
-
- {% if picture.asked_for_removal %}
- {% trans %}Asked for removal{% endtrans %}
- {% else %}
-
- {% endif %}
-
+ {% if not picture.is_moderated %}
+ {% set next = picture.get_next() %}
+ {% if not next %}
+ {% set next = url('sas:moderation') %}
+ {% else %}
+ {% set next = next.get_absolute_url() + "#pict" %}
{% endif %}
-
{% endblock %}
{% block script %}
- {{ super() }}
-
+ $(() => {
+ $(document).keydown((e) => {
+ switch (e.keyCode) {
+ case 37:
+ $('#prev a')[0].click();
+ break;
+ case 39:
+ $('#next a')[0].click();
+ break;
+ }
+ });
+ });
+
{% endblock %}
diff --git a/sas/tests/test_api.py b/sas/tests/test_api.py
index 917f73d0..58079e70 100644
--- a/sas/tests/test_api.py
+++ b/sas/tests/test_api.py
@@ -1,10 +1,12 @@
+from django.conf import settings
+from django.db import transaction
from django.test import TestCase
from django.urls import reverse
from model_bakery import baker
from model_bakery.recipe import Recipe
from core.baker_recipes import old_subscriber_user, subscriber_user
-from core.models import User
+from core.models import RealGroup, User
from sas.models import Album, PeoplePictureRelation, Picture
@@ -32,6 +34,8 @@ class TestSas(TestCase):
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_b)
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_c)
+
+class TestPictureSearch(TestSas):
def test_anonymous_user_forbidden(self):
res = self.client.get(reverse("api:pictures"))
assert res.status_code == 403
@@ -101,3 +105,49 @@ class TestSas(TestCase):
+ f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}"
)
assert res.status_code == 403
+
+
+class TestPictureRelation(TestSas):
+ def test_delete_relation_route_forbidden(self):
+ """Test that unauthorized users are properly 403ed"""
+ # take a picture where user_a isn't identified
+ relation = PeoplePictureRelation.objects.exclude(user=self.user_a).first()
+
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 403
+
+ for user in baker.make(User), self.user_a:
+ self.client.force_login(user)
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 403
+
+ def test_delete_relation_with_authorized_users(self):
+ """Test that deletion works as intended when called by an authorized user."""
+ relation: PeoplePictureRelation = self.user_a.pictures.first()
+ sas_admin_group = RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
+ sas_admin = baker.make(User, groups=[sas_admin_group])
+ root = baker.make(User, is_superuser=True)
+ for user in sas_admin, root, self.user_a:
+ with transaction.atomic():
+ self.client.force_login(user)
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 200
+ assert not PeoplePictureRelation.objects.filter(pk=relation.id).exists()
+ transaction.set_rollback(True)
+ public = baker.make(User)
+ relation = public.pictures.create(picture=relation.picture)
+ self.client.force_login(public)
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 200
+ assert not PeoplePictureRelation.objects.filter(pk=relation.id).exists()
+
+ def test_delete_twice(self):
+ """Test a duplicate call on the delete route."""
+ self.client.force_login(baker.make(User, is_superuser=True))
+ relation = PeoplePictureRelation.objects.first()
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 200
+ relation_count = PeoplePictureRelation.objects.count()
+ res = self.client.delete(f"/api/sas/relation/{relation.id}")
+ assert res.status_code == 404
+ assert PeoplePictureRelation.objects.count() == relation_count
diff --git a/sas/views.py b/sas/views.py
index 367771dc..9a885538 100644
--- a/sas/views.py
+++ b/sas/views.py
@@ -143,12 +143,6 @@ class PictureView(CanViewMixin, DetailView, FormMixin):
self.object.rotate(270)
if "rotate_left" in request.GET:
self.object.rotate(90)
- if "remove_user" in request.GET:
- user = get_object_or_404(User, pk=int(request.GET["remove_user"]))
- if user.id == request.user.id or request.user.is_in_group(
- pk=settings.SITH_GROUP_SAS_ADMIN_ID
- ):
- user.picture.filter(picture=self.object).delete()
if "ask_removal" in request.GET.keys():
self.object.is_moderated = False
self.object.asked_for_removal = True
diff --git a/sith/settings.py b/sith/settings.py
index 239720a4..7f2c4b0d 100644
--- a/sith/settings.py
+++ b/sith/settings.py
@@ -34,10 +34,9 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
"""
import binascii
-
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import sys
+from pathlib import Path
import sentry_sdk
from django.utils.translation import gettext_lazy as _
@@ -45,7 +44,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
from .honeypot import custom_honeypot_error
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = Path(__file__).parent.parent.resolve()
os.environ["HTTPS"] = "off"
@@ -212,7 +211,7 @@ REST_FRAMEWORK = {}
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
- "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+ "NAME": BASE_DIR / "db.sqlite3",
}
}
@@ -252,19 +251,19 @@ USE_I18N = True
USE_TZ = True
-LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
+LOCALE_PATHS = [BASE_DIR / "locale"]
PHONENUMBER_DEFAULT_REGION = "FR"
# Medias
-MEDIA_ROOT = "./data/"
MEDIA_URL = "/data/"
+MEDIA_ROOT = BASE_DIR / "data"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = "/static/"
-STATIC_ROOT = "./static/"
+STATIC_ROOT = BASE_DIR / "static"
# Static files finders which allow to see static folder in all apps
STATICFILES_FINDERS = [
@@ -288,7 +287,6 @@ HONEYPOT_VALUE = "content"
HONEYPOT_RESPONDER = custom_honeypot_error # Make honeypot errors less suspicious
HONEYPOT_FIELD_NAME_FORUM = "message2" # Only used on forum
-
# Email
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
EMAIL_HOST = "localhost"
@@ -725,10 +723,11 @@ if SENTRY_DSN:
environment=SENTRY_ENV,
)
-
SITH_FRONT_DEP_VERSIONS = {
+ "https://github.com/Stuk/jszip-utils": "0.1.0",
+ "https://github.com/Stuk/jszip": "3.10.1",
+ "https://github.com/jimmywarting/native-file-system-adapter": "3.0.1",
"https://github.com/chartjs/Chart.js/": "2.6.0",
- "https://github.com/xdan/datetimepicker/": "2.5.21",
"https://github.com/Ionaru/easy-markdown-editor/": "2.18.0",
"https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
"https://github.com/jquery/jquery/": "3.6.2",
@@ -736,7 +735,6 @@ SITH_FRONT_DEP_VERSIONS = {
"https://github.com/viralpatel/jquery.shorten/": "",
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
- "https://github.com/vuejs/vue-next": "3.2.18",
"https://github.com/alpinejs/alpine": "3.10.5",
"https://github.com/mrdoob/three.js/": "r148",
"https://github.com/vasturiano/three-spritetext": "1.6.5",
diff --git a/stock/templates/stock/shopping_list_items.jinja b/stock/templates/stock/shopping_list_items.jinja
index bafcd872..67cc5817 100644
--- a/stock/templates/stock/shopping_list_items.jinja
+++ b/stock/templates/stock/shopping_list_items.jinja
@@ -1,51 +1,51 @@
{% extends "core/base.jinja" %}
{% block title %}
-{% trans %}{{ shoppinglist }}'s items{% endtrans %}
+ {% trans %}{{ shoppinglist }}'s items{% endtrans %}
{% endblock %}
{% block content %}
-{% if current_tab == "stocks" %}
- {% trans %}Back{% endtrans %}
-{% endif %}
+ {% if current_tab == "stocks" %}
+ {% trans %}Back{% endtrans %}
+ {% endif %}
-
{{ shoppinglist.name }}
-{% for t in ProductType.objects.order_by('name').all() %}
- {% if shoppinglist.shopping_items_to_buy.filter(type=t) %}
-
{{ t }}
-
-
-
-
-
{% trans %}Name{% endtrans %}
-
{% trans %}Quantity asked{% endtrans %}
-
{% trans %}Quantity bought{% endtrans %}
-
-
-
- {% for i in shoppinglist.shopping_items_to_buy.filter(type=t).order_by('name').all() %}
-
-
{{ i.name }}
-
{{ i.tobuy_quantity }}
-
{{ i.bought_quantity }}
-
- {% endfor %}
-
-
- {% endif %}
-{% endfor %}
-
{% trans %}Other{% endtrans %}
-
-
-
-
-
{% trans %}Comments{% endtrans %}
-
-
-
-
-
{{ shoppinglist.comment }}
-
-
-
+
{{ shoppinglist.name }}
+ {% for t in ProductType.objects.order_by('name').all() %}
+ {% if shoppinglist.shopping_items_to_buy.filter(type=t) %}
+
{{ t }}
+
+
+
+
+
{% trans %}Name{% endtrans %}
+
{% trans %}Quantity asked{% endtrans %}
+
{% trans %}Quantity bought{% endtrans %}
+
+
+
+ {% for i in shoppinglist.shopping_items_to_buy.filter(type=t).order_by('name').all() %}
+
+
{{ i.name }}
+
{{ i.tobuy_quantity }}
+
{{ i.bought_quantity }}
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% endfor %}
+
{% trans %}Other{% endtrans %}
+
+
+
+
+
{% trans %}Comments{% endtrans %}
+
+
+
+
+
{{ shoppinglist.comment }}
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/stock/templates/stock/shopping_list_quantity.jinja b/stock/templates/stock/shopping_list_quantity.jinja
index 45a76d97..8ac66397 100644
--- a/stock/templates/stock/shopping_list_quantity.jinja
+++ b/stock/templates/stock/shopping_list_quantity.jinja
@@ -1,16 +1,16 @@
{% extends "core/base.jinja" %}
{% block title %}
-{% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}
+ {% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}
{% endblock %}
{% block content %}
-
{% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}
-
-
- {% csrf_token %}
- {{ form.as_p() }}
-
-
-
+
{% trans s = stock %}{{ s }}'s quantity to buy{% endtrans %}
{% trans s=stock %}Shopping lists history for {{ s }}{% endtrans %}
-
- {% trans %}Information :{% endtrans %}
-
- {% trans %}Use the "update stock" action when you get back from shopping to add the effective quantity bought for each shopping list item.{% endtrans %}
-
- {% trans %}For example, 3 Cheeseburger (boxes) are aksing in the list, but there were only 2 so, 2 have to be added in the stock quantity.{% endtrans %}
-
+
+ {% trans %}Information :{% endtrans %}
+
+ {% trans %}Use the "update stock" action when you get back from shopping to add the effective quantity bought for each shopping list item.{% endtrans %}
+
+ {% trans %}For example, 3 Cheeseburger (boxes) are aksing in the list, but there were only 2 so, 2 have to be added in the stock quantity.{% endtrans %}
+
-
{% trans %}To do{% endtrans %}
-
-
-
-
{% trans %}Date{% endtrans %}
-
{% trans %}Name{% endtrans %}
-
{% trans %}Number of items{% endtrans %}
-
-
-
- {% for s in stock.shopping_lists.filter(todo=True).filter(stock_owner=stock).order_by('-date').all() %}
-
{% endblock %}
\ No newline at end of file
diff --git a/stock/templates/stock/stock_take_items.jinja b/stock/templates/stock/stock_take_items.jinja
index ed59b3bc..742ad446 100644
--- a/stock/templates/stock/stock_take_items.jinja
+++ b/stock/templates/stock/stock_take_items.jinja
@@ -2,16 +2,16 @@
{% from 'core/macros.jinja' import user_profile_link %}
{% block title %}
-{% trans s = stock %}Take items from {{ s }}{% endtrans %}
+ {% trans s = stock %}Take items from {{ s }}{% endtrans %}
{% endblock %}
{% block content %}
-
{% trans s = stock %}Take items from {{ s }}{% endtrans %}
-
-
- {% csrf_token %}
- {{ form.as_p() }}
-
-
-
+
{% trans s = stock %}Take items from {{ s }}{% endtrans %}
+
+
+ {% csrf_token %}
+ {{ form.as_p() }}
+
+
+
{% endblock %}
diff --git a/stock/templates/stock/update_after_shopping.jinja b/stock/templates/stock/update_after_shopping.jinja
index fb043985..708ace05 100644
--- a/stock/templates/stock/update_after_shopping.jinja
+++ b/stock/templates/stock/update_after_shopping.jinja
@@ -1,16 +1,16 @@
{% extends "core/base.jinja" %}
{% block title %}
-{% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}
+ {% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}
{% endblock %}
{% block content %}
-
{% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}
-
-
- {% csrf_token %}
- {{ form.as_p() }}
-
-
-
+
{% trans s = shoppinglist %}Update {{ s }}'s quantity after shopping{% endtrans %}
+
+
+ {% csrf_token %}
+ {{ form.as_p() }}
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/subscription/templates/subscription/stats.jinja b/subscription/templates/subscription/stats.jinja
index 5db061a3..617705b9 100644
--- a/subscription/templates/subscription/stats.jinja
+++ b/subscription/templates/subscription/stats.jinja
@@ -1,136 +1,136 @@
{% extends "core/base.jinja" %}
{% block title %}
- {% trans %}Subscription stats{% endtrans %}
+ {% trans %}Subscription stats{% endtrans %}
{% endblock %}
{% block head %}
-{{ super() }}
-
+ {{ super() }}
+
{% endblock %}
{% block content %}
-
+
{{ form.start_date.label }}
{{ form.start_date }}
{{ form.end_date.label }}
{{ form.end_date }}
-
+
-
+
-
+
{% trans %}Total subscriptions{% endtrans %} : {{ subscriptions_total.count() }}
{% trans trombi = user.trombi_user.trombi %}You are subscribed to the Trombi {{ trombi }}{% endtrans %}
+
+ {% set can_comment = trombi.subscription_deadline < date.today() and
date.today() <= trombi.comments_deadline %}
-{% if not can_comment %}
-
{% trans %}You can not write comments at this date.{% endtrans %}
-
-{% trans start=trombi.subscription_deadline|date(DATE_FORMAT), end=trombi.comments_deadline|date(DATE_FORMAT) %}Comments are only allowed between {{ start }} (excluded) and {{ end }} (included){% endtrans %}
-
-{% endif %}
-
-{% for u in user.trombi_user.trombi.users.exclude(id=user.trombi_user.id).order_by('user__nick_name') %}
-
- {% set file = None %}
- {% if u.profile_pict %}
+ {% if not can_comment %}
+
{% trans %}You can not write comments at this date.{% endtrans %}
+
+ {% trans start=trombi.subscription_deadline|date(DATE_FORMAT), end=trombi.comments_deadline|date(DATE_FORMAT) %}Comments are only allowed between {{ start }} (excluded) and {{ end }} (included){% endtrans %}
+
+ {% endif %}
+
+ {% for u in user.trombi_user.trombi.users.exclude(id=user.trombi_user.id).order_by('user__nick_name') %}
+
+ {% set file = None %}
+ {% if u.profile_pict %}
{% set file = u.profile_pict.url %}
- {% else %}
+ {% else %}
{% set file = static('core/img/na.gif') %}
- {% endif %}
-
{% trans %}Report this comment{% endtrans %}
{% trans %}Report this comment{% endtrans %}
{{ comment.publish_date.strftime('%d/%m/%Y') }}
{{ comment.publish_date.strftime('%d/%m/%Y') }}
{{ user_profile_link(comment.author) }}
{{ user_profile_link(comment.author) }}