8 Commits

Author SHA1 Message Date
dependabot[bot]
79297b7a75 Bump vite from 6.3.5 to 6.3.6
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 02:04:00 +00:00
Kenneth Soares
e0702ce8be Merge pull request #1165 from ae-utbm/taiste
Commands, Galaxy, Buxfixes and other
2025-09-03 14:32:30 +02:00
thomas girod
f6683068ff Merge pull request #1147 from ae-utbm/taiste
Many fixes
2025-07-02 10:10:19 +02:00
thomas girod
81d1d1caca Merge pull request #1128 from ae-utbm/taiste
Api keys, better tabs, navbar and accordions, better notifications, fixes and dependencies updates
2025-06-17 14:08:05 +02:00
thomas girod
1cc2378476 Merge pull request #1112 from ae-utbm/taiste
Accordions, navbar and fixes
2025-06-05 19:51:13 +02:00
thomas girod
61e370cf73 Merge pull request #1107 from ae-utbm/taiste
Eboutic refactor, Celery, better tooltips, Python 3.13, bugfixes and other
2025-06-03 00:03:33 +02:00
thomas girod
6377acfffa Merge pull request #1084 from ae-utbm/taiste
Django 5.2, HTMX for billing infos form, eurocks widget consent message and new promo 24 logo
2025-04-14 12:42:19 +02:00
thomas girod
3c8933461a Merge pull request #1075 from ae-utbm/taiste
SAS and markdown pictures upload improval, google calendar removal, calendar export link, css fixes and more
2025-04-10 13:15:02 +02:00
34 changed files with 360 additions and 734 deletions

View File

@@ -26,16 +26,12 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.db.models import Exists, OuterRef, Q from django.db.models import Exists, OuterRef, Q
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from club.models import Club, Mailing, MailingSubscription, Membership from club.models import Club, Mailing, MailingSubscription, Membership
from core.models import User from core.models import User
from core.views.forms import SelectDateTime from core.views.forms import SelectDate, SelectDateTime
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import AutoCompleteSelectMultipleUser
AutoCompleteSelectMultipleUser,
AutoCompleteSelectUser,
)
from counter.models import Counter, Selling from counter.models import Counter, Selling
@@ -192,68 +188,70 @@ class SellingsForm(forms.Form):
) )
class ClubOldMemberForm(forms.Form): class ClubMemberForm(forms.Form):
members_old = forms.ModelMultipleChoiceField(
Membership.objects.none(),
label=_("Mark as old"),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, *args, user: User, club: Club, **kwargs):
super().__init__(*args, **kwargs)
self.fields["members_old"].queryset = (
Membership.objects.ongoing().filter(club=club).editable_by(user)
)
class ClubMemberForm(forms.ModelForm):
"""Form handling the members of a club.""" """Form handling the members of a club."""
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
class Meta: users = forms.ModelMultipleChoiceField(
model = Membership label=_("Users to add"),
fields = ["user", "role", "description"] help_text=_("Search users to add (one or more)."),
widgets = {"user": AutoCompleteSelectUser} required=False,
widget=AutoCompleteSelectMultipleUser,
queryset=User.objects.all(),
)
def __init__(self, *args, club: Club, request_user: User, **kwargs): def __init__(self, *args, **kwargs):
self.club = club self.club = kwargs.pop("club")
self.request_user = request_user self.request_user = kwargs.pop("request_user")
self.club_members = kwargs.pop("club_members", None)
if not self.club_members:
self.club_members = self.club.members.ongoing().order_by("-role").all()
self.request_user_membership = self.club.get_membership_for(self.request_user) self.request_user_membership = self.club.get_membership_for(self.request_user)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["role"].required = True
self.fields["role"].choices = [ # Using a ModelForm binds too much the form with the model and we don't want that
(value, name) # We want the view to process the model creation since they are multiple users
for value, name in settings.SITH_CLUB_ROLES.items() # We also want the form to handle bulk deletion
if value <= self.max_available_role self.fields.update(
forms.fields_for_model(
Membership,
fields=("role", "start_date", "description"),
widgets={"start_date": SelectDate},
)
)
# Role is required only if users is specified
self.fields["role"].required = False
# Start date and description are never really required
self.fields["start_date"].required = False
self.fields["description"].required = False
self.fields["users_old"] = forms.ModelMultipleChoiceField(
User.objects.filter(
id__in=[
ms.user.id
for ms in self.club_members
if ms.can_be_edited_by(self.request_user)
] ]
self.instance.club = club ).all(),
label=_("Mark as old"),
required=False,
widget=forms.CheckboxSelectMultiple,
)
if not self.request_user.is_root:
self.fields.pop("start_date")
@cached_property def clean_users(self):
def max_available_role(self): """Check that the user is not trying to add an user already in the club.
"""The greatest role that will be obtainable with this form.
Admins and the club president can attribute any role.
Board members can attribute roles lower than their own.
Other users can attribute curious and member roles.
"""
if self.request_user.has_perm("club.add_subscription"):
return settings.SITH_CLUB_ROLES_ID["President"]
membership = self.request_user_membership
if membership is not None and membership.role > settings.SITH_MAXIMUM_FREE_ROLE:
if membership.role == settings.SITH_CLUB_ROLES_ID["President"]:
return membership.role
return membership.role - 1
return settings.SITH_MAXIMUM_FREE_ROLE
def clean_user(self):
"""Check that the user is not trying to add a user already in the club.
Also check that the user is valid and has a valid subscription. Also check that the user is valid and has a valid subscription.
""" """
user = self.cleaned_data["user"] cleaned_data = super().clean()
users = []
for user in cleaned_data["users"]:
if not user.is_subscribed: if not user.is_subscribed:
raise forms.ValidationError( raise forms.ValidationError(
_("User must be subscriber to take part to a club"), code="invalid" _("User must be subscriber to take part to a club"), code="invalid"
@@ -262,24 +260,33 @@ class ClubMemberForm(forms.ModelForm):
raise forms.ValidationError( raise forms.ValidationError(
_("You can not add the same user twice"), code="invalid" _("You can not add the same user twice"), code="invalid"
) )
return user users.append(user)
return users
def clean(self): def clean(self):
"""Check user rights for adding a user.""" """Check user rights for adding an user."""
cleaned_data = super().clean() cleaned_data = super().clean()
if "role" not in cleaned_data:
if "start_date" in cleaned_data and not cleaned_data["start_date"]:
# Drop start_date if allowed to edition but not specified
cleaned_data.pop("start_date")
if not cleaned_data.get("users"):
# No user to add equals no check needed
return cleaned_data return cleaned_data
if (
self.request_user_membership is None if cleaned_data.get("role", "") == "":
or self.request_user_membership.role <= settings.SITH_MAXIMUM_FREE_ROLE # Role is required if users exists
) and not self.request_user.has_perm("club.add_membership"): self.add_error("role", _("You should specify a role"))
raise forms.ValidationError( return cleaned_data
_(
"You cannot add other users to a club " request_user = self.request_user
"if you are not in the club board." membership = self.request_user_membership
), if not (
code="invalid", cleaned_data["role"] <= settings.SITH_MAXIMUM_FREE_ROLE
) or (membership is not None and membership.role >= cleaned_data["role"])
if cleaned_data["role"] > self.max_available_role: or request_user.is_board_member
or request_user.is_root
):
raise forms.ValidationError(_("You do not have the permission to do that")) raise forms.ValidationError(_("You do not have the permission to do that"))
return cleaned_data return cleaned_data

View File

@@ -30,8 +30,7 @@ from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import RegexValidator, validate_email from django.core.validators import RegexValidator, validate_email
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Exists, F, OuterRef, Q, Value from django.db.models import Exists, F, OuterRef, Q
from django.db.models.functions import Greatest
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -201,6 +200,10 @@ class Club(models.Model):
"""Method to see if that object can be edited by the given user.""" """Method to see if that object can be edited by the given user."""
return self.has_rights_in_club(user) return self.has_rights_in_club(user)
def can_be_viewed_by(self, user: User) -> bool:
"""Method to see if that object can be seen by the given user."""
return user.was_subscribed
def get_membership_for(self, user: User) -> Membership | None: def get_membership_for(self, user: User) -> Membership | None:
"""Return the current membership the given user. """Return the current membership the given user.
@@ -240,41 +243,6 @@ class MembershipQuerySet(models.QuerySet):
""" """
return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE)
def editable_by(self, user: User) -> Self:
"""Filter Memberships that this user can edit.
Users with the `club.change_membership` permission can edit all Membership.
The other users can end :
- their own membership
- if they are board members, memberships with a role lower than their own
For example, let's suppose the following users :
- A : board member
- B : board member
- C : simple member
- D : curious
A will be able to end the memberships of A, C and D ;
C and D will be able to end only their own membership.
"""
if user.has_perm("club.change_membership"):
return self.all()
return self.filter(
Exists(
Membership.objects.filter(
Q(
role__gt=Greatest(
OuterRef("role"), Value(settings.SITH_MAXIMUM_FREE_ROLE)
)
)
| Q(pk=OuterRef("pk")),
user=user,
end_date=None,
club=OuterRef("club"),
)
)
)
def update(self, **kwargs) -> int: def update(self, **kwargs) -> int:
"""Refresh the cache and edit group ownership. """Refresh the cache and edit group ownership.
@@ -351,12 +319,16 @@ class Membership(models.Model):
User, User,
verbose_name=_("user"), verbose_name=_("user"),
related_name="memberships", related_name="memberships",
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
club = models.ForeignKey( club = models.ForeignKey(
Club, Club,
verbose_name=_("club"), verbose_name=_("club"),
related_name="members", related_name="members",
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
start_date = models.DateField(_("start date"), default=timezone.now) start_date = models.DateField(_("start date"), default=timezone.now)

View File

@@ -1,24 +0,0 @@
#club_members_table {
tbody label {
margin: 0;
padding: 0;
}
}
#add_club_members_form {
fieldset {
display: flex;
flex-direction: row;
column-gap: 2em;
row-gap: 1em;
flex-wrap: wrap;
@media (max-width: 1100px) {
justify-content: space-evenly;
}
.errorlist {
max-width: 300px;
}
}
}

View File

@@ -1,14 +1,6 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% block title -%}
{{ club.name }}
{%- endblock %}
{% block description -%}
{{ club.short_description }}
{%- endblock %}
{% block content %} {% block content %}
<div id="club_detail"> <div id="club_detail">
{% if club.logo %} {% if club.logo %}

View File

@@ -1,12 +1,8 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title -%} {% block title %}
{% trans %}Club list{% endtrans %} {% trans %}Club list{% endtrans %}
{%- endblock %} {% endblock %}
{% block description -%}
{% trans %}The list of all clubs existing at UTBM.{% endtrans %}
{%- endblock %}
{% macro display_club(club) -%} {% macro display_club(club) -%}
@@ -25,7 +21,7 @@
{%- if club.children.all()|length != 0 %} {%- if club.children.all()|length != 0 %}
<ul> <ul>
{%- for c in club.children.order_by('name').prefetch_related("children") %} {%- for c in club.children.order_by('name') %}
{{ display_club(c) }} {{ display_club(c) }}
{%- endfor %} {%- endfor %}
</ul> </ul>
@@ -40,8 +36,8 @@
{% if club_list %} {% if club_list %}
<h3>{% trans %}Club list{% endtrans %}</h3> <h3>{% trans %}Club list{% endtrans %}</h3>
<ul> <ul>
{%- for club in club_list %} {%- for c in club_list.all().order_by('name') if c.parent is none %}
{{ display_club(club) }} {{ display_club(c) }}
{%- endfor %} {%- endfor %}
</ul> </ul>
{% else %} {% else %}

View File

@@ -1,30 +1,15 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %} {% from 'core/macros.jinja' import user_profile_link, select_all_checkbox %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/core/components/ajax-select-index.ts") }}"></script>
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
<link rel="stylesheet" href="{{ static("club/members.scss") }}">
{% endblock %}
{% block content %} {% block content %}
<h2>{% trans %}Club members{% endtrans %}</h2> <h2>{% trans %}Club members{% endtrans %}</h2>
{% if add_member_fragment %}
<br />
<h4>{% trans %}Add a new member{% endtrans %}</h4>
{{ add_member_fragment }}
<br />
{% endif %}
{% if members %} {% if members %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="members_old" method="post"> <form action="{{ url('club:club_members', club_id=club.id) }}" id="users_old" method="post">
{% csrf_token %} {% csrf_token %}
{% if can_end_membership %} {% set users_old = dict(form.users_old | groupby("choice_label")) %}
{{ select_all_checkbox("members_old") }} {% if users_old %}
<br /> {{ select_all_checkbox("users_old") }}
<p></p>
{% endif %} {% endif %}
<table id="club_members_table"> <table id="club_members_table">
<thead> <thead>
@@ -33,7 +18,7 @@
<td>{% trans %}Role{% endtrans %}</td> <td>{% trans %}Role{% endtrans %}</td>
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}Since{% endtrans %}</td> <td>{% trans %}Since{% endtrans %}</td>
{% if can_end_membership %} {% if users_old %}
<td>{% trans %}Mark as old{% endtrans %}</td> <td>{% trans %}Mark as old{% endtrans %}</td>
{% endif %} {% endif %}
</tr> </tr>
@@ -45,24 +30,20 @@
<td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td> <td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
<td>{{ m.description }}</td> <td>{{ m.description }}</td>
<td>{{ m.start_date }}</td> <td>{{ m.start_date }}</td>
{%- if can_end_membership -%} {% if users_old %}
<td> <td>
{%- if m.is_editable -%} {% set user_old = users_old[m.user.get_display_name()] %}
<label for="id_members_old_{{ loop.index }}"></label> {% if user_old %}
<input {{ user_old[0].tag() }}
type="checkbox" {% endif %}
name="members_old"
value="{{ m.id }}"
id="id_members_old_{{ loop.index }}"
>
{%- endif -%}
</td> </td>
{%- endif -%} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
{% if can_end_membership %} {{ form.users_old.errors }}
{% if users_old %}
<p></p> <p></p>
<input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}"> <input type="submit" name="submit" value="{% trans %}Mark as old{% endtrans %}">
{% endif %} {% endif %}
@@ -70,4 +51,32 @@
{% else %} {% else %}
<p>{% trans %}There are no members in this club.{% endtrans %}</p> <p>{% trans %}There are no members in this club.{% endtrans %}</p>
{% endif %} {% endif %}
<form action="{{ url('club:club_members', club_id=club.id) }}" id="add_users" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
<p>
{{ form.users.errors }}
<label for="{{ form.users.id_for_label }}">{{ form.users.label }} :</label>
{{ form.users }}
<span class="helptext">{{ form.users.help_text }}</span>
</p>
<p>
{{ form.role.errors }}
<label for="{{ form.role.id_for_label }}">{{ form.role.label }} :</label>
{{ form.role }}
</p>
{% if form.start_date %}
<p>
{{ form.start_date.errors }}
<label for="{{ form.start_date.id_for_label }}">{{ form.start_date.label }} :</label>
{{ form.start_date }}
</p>
{% endif %}
<p>
{{ form.description.errors }}
<label for="{{ form.description.id_for_label }}">{{ form.description.label }} :</label>
{{ form.description }}
</p>
<p><input type="submit" value="{% trans %}Add{% endtrans %}" /></p>
</form>
{% endblock %} {% endblock %}

View File

@@ -5,22 +5,20 @@
<h2>{% trans %}Club old members{% endtrans %}</h2> <h2>{% trans %}Club old members{% endtrans %}</h2>
<table> <table>
<thead> <thead>
<tr>
<td>{% trans %}User{% endtrans %}</td> <td>{% trans %}User{% endtrans %}</td>
<td>{% trans %}Role{% endtrans %}</td> <td>{% trans %}Role{% endtrans %}</td>
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}From{% endtrans %}</td> <td>{% trans %}From{% endtrans %}</td>
<td>{% trans %}To{% endtrans %}</td> <td>{% trans %}To{% endtrans %}</td>
</tr>
</thead> </thead>
<tbody> <tbody>
{% for member in old_members %} {% for m in club.members.exclude(end_date=None).order_by('-role', 'description', '-end_date').all() %}
<tr> <tr>
<td>{{ user_profile_link(member.user) }}</td> <td>{{ user_profile_link(m.user) }}</td>
<td>{{ settings.SITH_CLUB_ROLES[member.role] }}</td> <td>{{ settings.SITH_CLUB_ROLES[m.role] }}</td>
<td>{{ member.description }}</td> <td>{{ m.description }}</td>
<td>{{ member.start_date }}</td> <td>{{ m.start_date }}</td>
<td>{{ member.end_date }}</td> <td>{{ m.end_date }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -1,44 +0,0 @@
{% if messages %}
<div x-data="{show_alert: true}" class="alert alert-green" x-show="show_alert" x-transition>
<span class="alert-main">
{% for message in messages %}
{% if message.level_tag == "success" %}
{{ message }}
{% endif %}
{% endfor %}
</span>
<span class="clickable" @click="show_alert = false">
<i class="fa fa-close"></i>
</span>
</div>
{% endif %}
<form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disabled-elt="find input[type='submit']"
hx-swap="outerHTML"
id="add_club_members_form"
>
{% csrf_token %}
{{ form.non_field_errors() }}
<fieldset>
<div>
{{ form.user.label_tag()}}
<span class="helptext">{{ form.user.help_text }}</span>
{{ form.user }}
{{ form.user.errors }}
</div>
<div>
{{ form.role.label_tag()}}
{{ form.role }}
{{ form.role.errors }}
</div>
<div>
{{ form.description.label_tag()}}
{{ form.description }}
{{ form.description.errors }}
</div>
</fieldset>
<button type="submit" class="btn btn-blue">
<i class="fa fa-user-plus"></i> {% trans %}Add{% endtrans %}</button>
</form>

View File

@@ -43,9 +43,6 @@ class TestClub(TestCase):
cls.ae = Club.objects.get(pk=settings.SITH_MAIN_CLUB_ID) cls.ae = Club.objects.get(pk=settings.SITH_MAIN_CLUB_ID)
cls.club = baker.make(Club) cls.club = baker.make(Club)
cls.new_members_url = reverse(
"club:club_new_members", kwargs={"club_id": cls.club.id}
)
cls.members_url = reverse("club:club_members", kwargs={"club_id": cls.club.id}) cls.members_url = reverse("club:club_members", kwargs={"club_id": cls.club.id})
a_month_ago = now() - timedelta(days=30) a_month_ago = now() - timedelta(days=30)
yesterday = now() - timedelta(days=1) yesterday = now() - timedelta(days=1)

View File

@@ -1,16 +1,13 @@
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.core.cache import cache from django.core.cache import cache
from django.db.models import Max from django.db.models import Max
from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import localdate, localtime, now from django.utils.timezone import localdate, localtime, now
from model_bakery import baker from model_bakery import baker
from pytest_django.asserts import assertRedirects
from club.forms import ClubMemberForm from club.forms import ClubMemberForm
from club.models import Club, Membership from club.models import Membership
from club.tests.base import TestClub 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
@@ -140,38 +137,6 @@ class TestMembershipQuerySet(TestClub):
assert set(user.groups.all()).isdisjoint(club_groups) assert set(user.groups.all()).isdisjoint(club_groups)
class TestMembershipEditableBy(TestCase):
@classmethod
def setUpTestData(cls):
Membership.objects.all().delete()
cls.club_a, cls.club_b = baker.make(Club, _quantity=2)
cls.memberships = [
*baker.make(
Membership, role=iter([7, 3, 3, 1]), club=cls.club_a, _quantity=4
),
*baker.make(
Membership, role=iter([7, 3, 3, 1]), club=cls.club_b, _quantity=4
),
]
def test_admin_user(self):
perm = Permission.objects.get(codename="change_membership")
user = baker.make(User, user_permissions=[perm])
qs = Membership.objects.editable_by(user).values_list("id", flat=True)
assert set(qs) == set(Membership.objects.values_list("id", flat=True))
def test_simple_subscriber_user(self):
user = subscriber_user.make()
assert not Membership.objects.editable_by(user).exists()
def test_board_member(self):
# a board member can end lower memberships and its own one
user = self.memberships[2].user
qs = Membership.objects.editable_by(user).values_list("id", flat=True)
expected = {self.memberships[2].id, self.memberships[3].id}
assert set(qs) == expected
class TestMembership(TestClub): class TestMembership(TestClub):
def assert_membership_started_today(self, user: User, role: int): def assert_membership_started_today(self, user: User, role: int):
"""Assert that the given membership is active and started today.""" """Assert that the given membership is active and started today."""
@@ -186,7 +151,7 @@ class TestMembership(TestClub):
def assert_membership_ended_today(self, user: User): def assert_membership_ended_today(self, user: User):
"""Assert that the given user have a membership which ended today.""" """Assert that the given user have a membership which ended today."""
today = localdate() today = localtime(now()).date()
assert user.memberships.filter(club=self.club, end_date=today).exists() assert user.memberships.filter(club=self.club, end_date=today).exists()
assert self.club.get_membership_for(user) is None assert self.club.get_membership_for(user) is None
@@ -195,9 +160,7 @@ class TestMembership(TestClub):
cannot see the page. cannot see the page.
""" """
response = self.client.post(self.members_url) response = self.client.post(self.members_url)
assertRedirects( assert response.status_code == 403
response, reverse("core:login", query={"next": self.members_url})
)
self.client.force_login(self.public) self.client.force_login(self.public)
response = self.client.post(self.members_url) response = self.client.post(self.members_url)
@@ -208,9 +171,7 @@ class TestMembership(TestClub):
information are displayed. information are displayed.
""" """
self.client.force_login(self.simple_board_member) self.client.force_login(self.simple_board_member)
response = self.client.get( response = self.client.get(self.members_url)
reverse("club:club_members", kwargs={"club_id": self.club.id})
)
assert response.status_code == 200 assert response.status_code == 200
soup = BeautifulSoup(response.text, "lxml") soup = BeautifulSoup(response.text, "lxml")
table = soup.find("table", id="club_members_table") table = soup.find("table", id="club_members_table")
@@ -236,45 +197,59 @@ class TestMembership(TestClub):
assert cols[2].text == membership.description assert cols[2].text == membership.description
assert cols[3].text == str(membership.start_date) assert cols[3].text == str(membership.start_date)
if membership.role < 3 or membership.user_id == self.simple_board_member.id: if membership.role <= 3: # 3 is the role of simple_board_member
# 3 is the role of simple_board_member
form_input = cols[4].find("input") form_input = cols[4].find("input")
expected_attrs = { expected_attrs = {
"type": "checkbox", "type": "checkbox",
"name": "members_old", "name": "users_old",
"value": str(membership.id), "value": str(user.id),
} }
assert form_input.attrs.items() >= expected_attrs.items() assert form_input.attrs.items() >= expected_attrs.items()
else: else:
assert cols[4].find_all() == [] assert cols[4].find_all() == []
def test_root_add_one_club_member(self): def test_root_add_one_club_member(self):
"""Test that root users can add members to clubs""" """Test that root users can add members to clubs, one at a time."""
self.client.force_login(self.root) self.client.force_login(self.root)
response = self.client.post( response = self.client.post(
self.new_members_url, {"user": self.subscriber.id, "role": 3} self.members_url,
) {"users": [self.subscriber.id], "role": 3},
assert response.status_code == 200
assert response.headers.get("HX-Redirect", "") == reverse(
"club:club_members", kwargs={"club_id": self.club.id}
) )
self.assertRedirects(response, self.members_url)
self.subscriber.refresh_from_db() self.subscriber.refresh_from_db()
self.assert_membership_started_today(self.subscriber, role=3) self.assert_membership_started_today(self.subscriber, role=3)
def test_root_add_multiple_club_member(self):
"""Test that root users can add multiple members at once to clubs."""
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
{
"users": (self.subscriber.id, self.krophil.id),
"role": 3,
},
)
self.assertRedirects(response, self.members_url)
self.subscriber.refresh_from_db()
self.assert_membership_started_today(self.subscriber, role=3)
self.assert_membership_started_today(self.krophil, role=3)
def test_add_unauthorized_members(self): def test_add_unauthorized_members(self):
"""Test that users who are not currently subscribed """Test that users who are not currently subscribed
cannot be members of clubs. cannot be members of clubs.
""" """
for user in self.public, self.old_subscriber: for user in self.public, self.old_subscriber:
form = ClubMemberForm( form = ClubMemberForm(
data={"user": user.id, "role": 1}, data={"users": [user.id], "role": 1},
request_user=self.root, request_user=self.root,
club=self.club, club=self.club,
) )
assert not form.is_valid() assert not form.is_valid()
assert form.errors == { assert form.errors == {
"user": ["L'utilisateur doit être cotisant pour faire partie d'un club"] "users": [
"L'utilisateur doit être cotisant pour faire partie d'un club"
]
} }
def test_add_members_already_members(self): def test_add_members_already_members(self):
@@ -307,15 +282,15 @@ class TestMembership(TestClub):
max_id = User.objects.aggregate(id=Max("id"))["id"] max_id = User.objects.aggregate(id=Max("id"))["id"]
for members in [max_id + 1], [max_id + 1, self.subscriber.id]: for members in [max_id + 1], [max_id + 1, self.subscriber.id]:
form = ClubMemberForm( form = ClubMemberForm(
data={"user": members, "role": 1}, data={"users": members, "role": 1},
request_user=self.root, request_user=self.root,
club=self.club, club=self.club,
) )
assert not form.is_valid() assert not form.is_valid()
assert form.errors == { assert form.errors == {
"user": [ "users": [
"Sélectionnez un choix valide. " "Sélectionnez un choix valide. "
"Ce choix ne fait pas partie de ceux disponibles." f"{max_id + 1} n\u2019en fait pas partie."
] ]
} }
self.club.refresh_from_db() self.club.refresh_from_db()
@@ -328,12 +303,10 @@ class TestMembership(TestClub):
nb_subscriber_memberships = self.subscriber.memberships.count() nb_subscriber_memberships = self.subscriber.memberships.count()
self.client.force_login(president) self.client.force_login(president)
response = self.client.post( response = self.client.post(
self.new_members_url, {"user": self.subscriber.id, "role": 9} self.members_url,
) {"users": self.subscriber.id, "role": 9},
assert response.status_code == 200
assert response.headers.get("HX-Redirect", "") == reverse(
"club:club_members", kwargs={"club_id": self.club.id}
) )
self.assertRedirects(response, self.members_url)
self.club.refresh_from_db() self.club.refresh_from_db()
self.subscriber.refresh_from_db() self.subscriber.refresh_from_db()
assert self.club.members.count() == nb_club_membership + 1 assert self.club.members.count() == nb_club_membership + 1
@@ -345,7 +318,7 @@ class TestMembership(TestClub):
a membership with a greater role than its own. a membership with a greater role than its own.
""" """
form = ClubMemberForm( form = ClubMemberForm(
data={"user": self.subscriber.id, "role": 10}, data={"users": [self.subscriber.id], "role": 10},
request_user=self.simple_board_member, request_user=self.simple_board_member,
club=self.club, club=self.club,
) )
@@ -353,7 +326,7 @@ class TestMembership(TestClub):
assert not form.is_valid() assert not form.is_valid()
assert form.errors == { assert form.errors == {
"role": ["Sélectionnez un choix valide. 10 n\u2019en fait pas partie."] "__all__": ["Vous n'avez pas la permission de faire cela"]
} }
self.club.refresh_from_db() self.club.refresh_from_db()
assert nb_memberships == self.club.members.count() assert nb_memberships == self.club.members.count()
@@ -361,18 +334,23 @@ class TestMembership(TestClub):
def test_add_member_without_role(self): def test_add_member_without_role(self):
"""Test that trying to add members without specifying their role fails.""" """Test that trying to add members without specifying their role fails."""
self.client.force_login(self.root)
form = ClubMemberForm( form = ClubMemberForm(
data={"user": self.subscriber.id}, request_user=self.root, club=self.club data={"users": [self.subscriber.id]},
request_user=self.simple_board_member,
club=self.club,
) )
assert not form.is_valid() assert not form.is_valid()
assert form.errors == {"role": ["Ce champ est obligatoire."]} assert form.errors == {"role": ["Vous devez choisir un rôle"]}
def test_end_membership_self(self): def test_end_membership_self(self):
"""Test that a member can end its own membership.""" """Test that a member can end its own membership."""
self.client.force_login(self.simple_board_member) self.client.force_login(self.simple_board_member)
membership = self.club.members.get(end_date=None, user=self.simple_board_member) self.client.post(
self.client.post(self.members_url, {"members_old": [membership.id]}) self.members_url,
{"users_old": self.simple_board_member.id},
)
self.simple_board_member.refresh_from_db() self.simple_board_member.refresh_from_db()
self.assert_membership_ended_today(self.simple_board_member) self.assert_membership_ended_today(self.simple_board_member)
@@ -380,13 +358,15 @@ class TestMembership(TestClub):
"""Test that board members of the club can end memberships """Test that board members of the club can end memberships
of users with lower roles. of users with lower roles.
""" """
# reminder : simple_board_member has role 3 # remainder : simple_board_member has role 3, president has role 10, richard has role 1
self.client.force_login(self.simple_board_member) self.client.force_login(self.simple_board_member)
membership = baker.make(Membership, club=self.club, role=2, end_date=None) response = self.client.post(
response = self.client.post(self.members_url, {"members_old": [membership.id]}) self.members_url,
{"users_old": self.richard.id},
)
self.assertRedirects(response, self.members_url) self.assertRedirects(response, self.members_url)
self.club.refresh_from_db() self.club.refresh_from_db()
self.assert_membership_ended_today(membership.user) self.assert_membership_ended_today(self.richard)
def test_end_membership_higher_role(self): def test_end_membership_higher_role(self):
"""Test that board members of the club cannot end memberships """Test that board members of the club cannot end memberships
@@ -394,30 +374,46 @@ class TestMembership(TestClub):
""" """
membership = self.president.memberships.filter(club=self.club).first() membership = self.president.memberships.filter(club=self.club).first()
self.client.force_login(self.simple_board_member) self.client.force_login(self.simple_board_member)
self.client.post(self.members_url, {"members_old": [membership.id]}) self.client.post(
self.members_url,
{"users_old": self.president.id},
)
self.club.refresh_from_db() self.club.refresh_from_db()
new_membership = self.club.get_membership_for(self.president) new_membership = self.club.get_membership_for(self.president)
assert new_membership is not None assert new_membership is not None
assert new_membership == membership assert new_membership == membership
membership.refresh_from_db() membership = self.president.memberships.filter(club=self.club).first()
assert membership.end_date is None assert membership.end_date is None
def test_end_membership_with_permission(self): def test_end_membership_as_main_club_board(self):
"""Test that users with permission can end any membership.""" """Test that board members of the main club can end the membership
of anyone.
"""
# make subscriber a board member # make subscriber a board member
subscriber = subscriber_user.make()
Membership.objects.create(club=self.ae, user=subscriber, role=3)
nb_memberships = self.club.members.ongoing().count() nb_memberships = self.club.members.ongoing().count()
self.client.force_login( self.client.force_login(subscriber)
subscriber_user.make(
user_permissions=[Permission.objects.get(codename="change_membership")]
)
)
president_membership = self.club.president
response = self.client.post( response = self.client.post(
self.members_url, {"members_old": [president_membership.id]} self.members_url,
{"users_old": self.president.id},
) )
self.assertRedirects(response, self.members_url) self.assertRedirects(response, self.members_url)
self.assert_membership_ended_today(president_membership.user) self.assert_membership_ended_today(self.president)
assert self.club.members.ongoing().count() == nb_memberships - 1
def test_end_membership_as_root(self):
"""Test that root users can end the membership of anyone."""
nb_memberships = self.club.members.ongoing().count()
self.client.force_login(self.root)
response = self.client.post(
self.members_url,
{"users_old": [self.president.id]},
)
self.assertRedirects(response, self.members_url)
self.assert_membership_ended_today(self.president)
assert self.club.members.ongoing().count() == nb_memberships - 1 assert self.club.members.ongoing().count() == nb_memberships - 1
def test_end_membership_as_foreigner(self): def test_end_membership_as_foreigner(self):
@@ -425,11 +421,14 @@ class TestMembership(TestClub):
nb_memberships = self.club.members.count() nb_memberships = self.club.members.count()
membership = self.richard.memberships.filter(club=self.club).first() membership = self.richard.memberships.filter(club=self.club).first()
self.client.force_login(self.subscriber) self.client.force_login(self.subscriber)
self.client.post(self.members_url, {"members_old": [self.richard.id]}) self.client.post(
self.members_url,
{"users_old": [self.richard.id]},
)
# nothing should have changed # nothing should have changed
membership.refresh_from_db() new_mem = self.club.get_membership_for(self.richard)
assert self.club.members.count() == nb_memberships assert self.club.members.count() == nb_memberships
assert membership.end_date is None assert membership == new_mem
def test_remove_from_club_group(self): def test_remove_from_club_group(self):
"""Test that when a membership ends, the user is removed from club groups.""" """Test that when a membership ends, the user is removed from club groups."""

View File

@@ -25,7 +25,6 @@
from django.urls import path from django.urls import path
from club.views import ( from club.views import (
ClubAddMembersFragment,
ClubCreateView, ClubCreateView,
ClubEditView, ClubEditView,
ClubListView, ClubListView,
@@ -61,11 +60,6 @@ urlpatterns = [
path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"), path("<int:club_id>/edit/", ClubEditView.as_view(), name="club_edit"),
path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"), path("<int:club_id>/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"),
path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"), path("<int:club_id>/members/", ClubMembersView.as_view(), name="club_members"),
path(
"fragment/<int:club_id>/members/",
ClubAddMembersFragment.as_view(),
name="club_new_members",
),
path( path(
"<int:club_id>/elderlies/", "<int:club_id>/elderlies/",
ClubOldMembersView.as_view(), ClubOldMembersView.as_view(),

View File

@@ -23,14 +23,12 @@
# #
import csv import csv
from typing import Any
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator from django.core.paginator import InvalidPage, Paginator
from django.db.models import Q, Sum from django.db.models import Sum
from django.http import ( from django.http import (
Http404, Http404,
HttpResponseRedirect, HttpResponseRedirect,
@@ -39,8 +37,7 @@ from django.http import (
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.safestring import SafeString from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext as _t from django.utils.translation import gettext as _t
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View from django.views.generic import DetailView, ListView, View
@@ -50,26 +47,20 @@ from club.forms import (
ClubAdminEditForm, ClubAdminEditForm,
ClubEditForm, ClubEditForm,
ClubMemberForm, ClubMemberForm,
ClubOldMemberForm,
MailingForm, MailingForm,
SellingsForm, SellingsForm,
) )
from club.models import ( from club.models import Club, Mailing, MailingSubscription, Membership
Club,
Mailing,
MailingSubscription,
Membership,
)
from com.views import ( from com.views import (
PosterCreateBaseView, PosterCreateBaseView,
PosterDeleteBaseView, PosterDeleteBaseView,
PosterEditBaseView, PosterEditBaseView,
PosterListBaseView, PosterListBaseView,
) )
from core.auth.mixins import CanCreateMixin, CanEditMixin from core.auth.mixins import CanCreateMixin, CanEditMixin, CanViewMixin
from core.models import PageRev from core.models import PageRev
from core.views import DetailFormView, PageEditViewBase, UseFragmentsMixin from core.views import DetailFormView, PageEditViewBase
from core.views.mixins import FragmentMixin, FragmentRenderer, TabedViewMixin from core.views.mixins import TabedViewMixin
from counter.models import Selling from counter.models import Selling
@@ -88,7 +79,7 @@ class ClubTabsMixin(TabedViewMixin):
"name": _("Infos"), "name": _("Infos"),
} }
] ]
if self.request.user.has_perm("club.view_club"): if self.request.user.can_view(self.object):
tab_list.extend( tab_list.extend(
[ [
{ {
@@ -180,10 +171,6 @@ class ClubListView(ListView):
model = Club model = Club
template_name = "club/club_list.jinja" template_name = "club/club_list.jinja"
queryset = (
Club.objects.filter(parent=None).order_by("name").prefetch_related("children")
)
context_object_name = "club_list"
class ClubView(ClubTabsMixin, DetailView): class ClubView(ClubTabsMixin, DetailView):
@@ -237,14 +224,13 @@ class ClubPageEditView(ClubTabsMixin, PageEditViewBase):
return reverse_lazy("club:club_view", kwargs={"club_id": self.club.id}) return reverse_lazy("club:club_view", kwargs={"club_id": self.club.id})
class ClubPageHistView(ClubTabsMixin, PermissionRequiredMixin, DetailView): class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView):
"""Modification hostory of the page.""" """Modification hostory of the page."""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
template_name = "club/page_history.jinja" template_name = "club/page_history.jinja"
current_tab = "history" current_tab = "history"
permission_required = "club.view_club"
class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
@@ -256,109 +242,57 @@ class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView):
current_tab = "tools" current_tab = "tools"
class ClubAddMembersFragment( class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView):
FragmentMixin, PermissionRequiredMixin, SuccessMessageMixin, CreateView
):
template_name = "club/fragments/add_member.jinja"
form_class = ClubMemberForm
model = Membership
object = None
reload_on_redirect = True
permission_required = "club.view_club"
success_message = _("%(user)s has been added to club.")
def dispatch(self, *args, **kwargs):
club_id = self.kwargs.get("club_id")
if not club_id:
raise Http404
self.club = get_object_or_404(Club, pk=kwargs.get("club_id"))
return super().dispatch(*args, **kwargs)
def get_form_kwargs(self):
return super().get_form_kwargs() | {
"request_user": self.request.user,
"club": self.club,
}
def render_fragment(self, request, **kwargs) -> SafeString:
self.club = kwargs.get("club")
return super().render_fragment(request, **kwargs)
def get_success_url(self):
return reverse("club:club_members", kwargs={"club_id": self.club.id})
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"club": self.club}
class ClubMembersView(
ClubTabsMixin, UseFragmentsMixin, PermissionRequiredMixin, DetailFormView
):
"""View of a club's members.""" """View of a club's members."""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
form_class = ClubOldMemberForm form_class = ClubMemberForm
template_name = "club/club_members.jinja" template_name = "club/club_members.jinja"
current_tab = "members" current_tab = "members"
permission_required = "club.view_club"
def get_fragments(self) -> dict[str, type[FragmentMixin] | FragmentRenderer]: @cached_property
membership = self.object.get_membership_for(self.request.user) def members(self) -> list[Membership]:
if membership and membership.role <= settings.SITH_MAXIMUM_FREE_ROLE: return list(self.object.members.ongoing().order_by("-role"))
return {}
return {"add_member_fragment": ClubAddMembersFragment}
def get_fragment_data(self) -> dict[str, Any]:
return {"add_member_fragment": {"club": self.object}}
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user kwargs["request_user"] = self.request.user
kwargs["club"] = self.object kwargs["club"] = self.object
kwargs["club_members"] = self.members
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
editable = list( kwargs["members"] = self.members
kwargs["form"].fields["members_old"].queryset.values_list("id", flat=True)
)
kwargs["members"] = list(
self.object.members.ongoing()
.annotate(is_editable=Q(id__in=editable))
.order_by("-role")
.select_related("user")
)
kwargs["can_end_membership"] = len(editable) > 0
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
for membership in form.cleaned_data.get("members_old"): """Check user rights."""
membership.end_date = now() resp = super().form_valid(form)
data = form.clean()
users = data.pop("users", [])
users_old = data.pop("users_old", [])
for user in users:
Membership(club=self.object, user=user, **data).save()
for user in users_old:
membership = self.object.get_membership_for(user)
membership.end_date = timezone.now()
membership.save() membership.save()
return super().form_valid(form) return resp
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return self.request.path return self.request.path
class ClubOldMembersView(ClubTabsMixin, PermissionRequiredMixin, DetailView): class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView):
"""Old members of a club.""" """Old members of a club."""
model = Club model = Club
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
template_name = "club/club_old_members.jinja" template_name = "club/club_old_members.jinja"
current_tab = "elderlies" current_tab = "elderlies"
permission_required = "club.view_club"
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {
"old_members": (
self.object.members.exclude(end_date=None)
.order_by("-role", "description", "-end_date")
.select_related("user")
)
}
class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView):
@@ -748,11 +682,9 @@ class MailingAutoGenerationView(View):
return redirect("club:mailing", club_id=club.id) return redirect("club:mailing", club_id=club.id)
class PosterListView(ClubTabsMixin, PermissionRequiredMixin, PosterListBaseView): class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin):
"""List communication posters.""" """List communication posters."""
permission_required = "club.view_club"
def get_object(self): def get_object(self):
return self.club return self.club
@@ -768,7 +700,7 @@ class PosterCreateView(PosterCreateBaseView, CanCreateMixin):
pk_url_kwarg = "club_id" pk_url_kwarg = "club_id"
def get_object(self, *args, **kwargs): def get_object(self):
obj = super().get_object() obj = super().get_object()
if not obj: if not obj:
return self.club return self.club

View File

@@ -68,7 +68,7 @@ class IcsCalendar:
start=news_date.start_date, start=news_date.start_date,
end=news_date.end_date, end=news_date.end_date,
url=as_absolute_url( url=as_absolute_url(
reverse("com:news_detail", kwargs={"news_id": news_date.news_id}) reverse("com:news_detail", kwargs={"news_id": news_date.news.id})
), ),
) )
calendar.events.append(event) calendar.events.append(event)

View File

@@ -27,7 +27,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Exists, F, OuterRef, Q from django.db.models import F, Q
from django.shortcuts import render from django.shortcuts import render
from django.templatetags.static import static from django.templatetags.static import static
from django.urls import reverse from django.urls import reverse
@@ -55,17 +55,9 @@ class Sith(models.Model):
class NewsQuerySet(models.QuerySet): class NewsQuerySet(models.QuerySet):
def published(self) -> Self: def moderated(self) -> Self:
return self.filter(is_published=True) return self.filter(is_published=True)
def waiting_moderation(self) -> Self:
"""Filter all non-finished non-published news"""
# Because of the way News and NewsDates are created,
# there may be some cases where this method is called before
# the NewsDates linked to a Date are actually persisted in db.
# Thus, it's important to filter by "not past date" rather than by "future date"
return self.filter(~Q(dates__start_date__lt=timezone.now()), is_published=False)
def viewable_by(self, user: User) -> Self: def viewable_by(self, user: User) -> Self:
"""Filter news that the given user can view. """Filter news that the given user can view.
@@ -135,28 +127,20 @@ class News(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
if not self.is_published: if self.is_published:
admins_without_notif = User.objects.filter( return
~Exists( for user in User.objects.filter(
Notification.objects.filter( groups__id__in=[settings.SITH_GROUP_COM_ADMIN_ID]
user=OuterRef("pk"), type="NEWS_MODERATION" ):
Notification.objects.create(
user=user, url=reverse("com:news_admin_list"), type="NEWS_MODERATION"
) )
),
groups__id=settings.SITH_GROUP_COM_ADMIN_ID,
)
notif_url = reverse("com:news_admin_list")
new_notifs = [
Notification(user=user, url=notif_url, type="NEWS_MODERATION")
for user in admins_without_notif
]
Notification.objects.bulk_create(new_notifs)
self.update_moderation_notifs()
def get_absolute_url(self): def get_absolute_url(self):
return reverse("com:news_detail", kwargs={"news_id": self.id}) return reverse("com:news_detail", kwargs={"news_id": self.id})
def get_full_url(self): def get_full_url(self):
return f"https://{settings.SITH_URL}{self.get_absolute_url()}" return "https://%s%s" % (settings.SITH_URL, self.get_absolute_url())
def is_owned_by(self, user): def is_owned_by(self, user):
if user.is_anonymous: if user.is_anonymous:
@@ -175,16 +159,19 @@ class News(models.Model):
or (user.is_authenticated and self.author_id == user.id) or (user.is_authenticated and self.author_id == user.id)
) )
@staticmethod
def update_moderation_notifs(): def news_notification_callback(notif: Notification):
count = News.objects.waiting_moderation().count() # the NewsDate linked to the News
notifs_qs = Notification.objects.filter( # which creation triggered this callback may not exist yet,
type="NEWS_MODERATION", user__groups__id=settings.SITH_GROUP_COM_ADMIN_ID # so it's important to filter by "not past date" rather than by "future date"
) count = News.objects.filter(
~Q(dates__start_date__gt=timezone.now()), is_published=False
).count()
if count: if count:
notifs_qs.update(viewed=False, param=str(count)) notif.viewed = False
notif.param = str(count)
else: else:
notifs_qs.update(viewed=True) notif.viewed = True
class NewsDateQuerySet(models.QuerySet): class NewsDateQuerySet(models.QuerySet):

View File

@@ -1,6 +1,10 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "com/macros.jinja" import news_moderation_alert %} {% from "com/macros.jinja" import news_moderation_alert %}
{% block title %}
{% trans %}News{% endtrans %}
{% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('com/css/news-list.scss') }}"> <link rel="stylesheet" href="{{ static('com/css/news-list.scss') }}">
<link rel="stylesheet" href="{{ static('com/components/ics-calendar.scss') }}"> <link rel="stylesheet" href="{{ static('com/components/ics-calendar.scss') }}">

View File

@@ -1,22 +1,13 @@
from datetime import timedelta
import pytest import pytest
from django.conf import settings from django.conf import settings
from django.utils.timezone import now
from model_bakery import baker from model_bakery import baker
from com.models import News, NewsDate from com.models import News
from core.baker_recipes import subscriber_user
from core.models import Group, Notification, User from core.models import Group, Notification, User
@pytest.mark.django_db @pytest.mark.django_db
def test_notification_created(): def test_notification_created():
# this news is unpublished, but is set in the past
# it shouldn't be taken into account when counting the number
# of news that are to be moderated
past_news = baker.make(News, is_published=False)
baker.make(NewsDate, news=past_news, start_date=now() - timedelta(days=1))
com_admin_group = Group.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID) com_admin_group = Group.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID)
com_admin_group.users.all().delete() com_admin_group.users.all().delete()
Notification.objects.all().delete() Notification.objects.all().delete()
@@ -24,28 +15,9 @@ def test_notification_created():
for i in range(2): for i in range(2):
# news notifications are permanent, so the notification created # news notifications are permanent, so the notification created
# during the first iteration should be reused during the second one. # during the first iteration should be reused during the second one.
baker.make(News, is_published=False) baker.make(News)
notifications = list(Notification.objects.all()) notifications = list(Notification.objects.all())
assert len(notifications) == 1 assert len(notifications) == 1
assert notifications[0].user == com_admin assert notifications[0].user == com_admin
assert notifications[0].type == "NEWS_MODERATION" assert notifications[0].type == "NEWS_MODERATION"
assert notifications[0].param == str(i + 1) assert notifications[0].param == str(i + 1)
@pytest.mark.django_db
def test_notification_edited_when_moderating_news():
com_admin_group = Group.objects.get(pk=settings.SITH_GROUP_COM_ADMIN_ID)
com_admins = subscriber_user.make(_quantity=3)
com_admin_group.users.set(com_admins)
Notification.objects.all().delete()
news = baker.make(News, is_published=False)
assert Notification.objects.count() == 3
assert Notification.objects.filter(viewed=False).count() == 3
news.is_published = True
news.moderator = com_admins[0]
news.save()
# when the news is moderated, the notification should be marked as read
# for all admins
assert Notification.objects.count() == 3
assert Notification.objects.filter(viewed=False).count() == 0

View File

@@ -560,7 +560,7 @@ class User(AbstractUser):
"""Determine if the object is owned by the user.""" """Determine if the object is owned by the user."""
if hasattr(obj, "is_owned_by") and obj.is_owned_by(self): if hasattr(obj, "is_owned_by") and obj.is_owned_by(self):
return True return True
if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group_id): if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id):
return True return True
return self.is_root return self.is_root
@@ -569,14 +569,8 @@ class User(AbstractUser):
if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self): if hasattr(obj, "can_be_edited_by") and obj.can_be_edited_by(self):
return True return True
if hasattr(obj, "edit_groups"): if hasattr(obj, "edit_groups"):
if ( for pk in obj.edit_groups.values_list("pk", flat=True):
hasattr(obj, "_prefetched_objects_cache") if self.is_in_group(pk=pk):
and "edit_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.edit_groups.all()]
else:
pks = list(obj.edit_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True return True
if isinstance(obj, User) and obj == self: if isinstance(obj, User) and obj == self:
return True return True
@@ -587,17 +581,8 @@ class User(AbstractUser):
if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self):
return True return True
if hasattr(obj, "view_groups"): if hasattr(obj, "view_groups"):
# if "view_groups" has already been prefetched, use for pk in obj.view_groups.values_list("pk", flat=True):
# the prefetch cache, else fetch only the ids, to make if self.is_in_group(pk=pk):
# the query lighter.
if (
hasattr(obj, "_prefetched_objects_cache")
and "view_groups" in obj._prefetched_objects_cache
):
pks = [g.id for g in obj.view_groups.all()]
else:
pks = list(obj.view_groups.values_list("id", flat=True))
if any(self.is_in_group(pk=pk) for pk in pks):
return True return True
return self.can_edit(obj) return self.can_edit(obj)
@@ -1399,9 +1384,9 @@ class Page(models.Model):
@cached_property @cached_property
def is_club_page(self): def is_club_page(self):
return ( club_root_page = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first()
self.name == settings.SITH_CLUB_ROOT_PAGE return club_root_page is not None and (
or settings.SITH_CLUB_ROOT_PAGE in [p.name for p in self.get_parent_list()] self == club_root_page or club_root_page in self.get_parent_list()
) )
@cached_property @cached_property

View File

@@ -36,7 +36,6 @@
> .ts-control { > .ts-control {
box-shadow: none; box-shadow: none;
max-width: 300px; max-width: 300px;
width: 300px;
background-color: var(--nf-input-background-color); background-color: var(--nf-input-background-color);
&::after { &::after {

View File

@@ -47,7 +47,6 @@
} }
input, input,
select,
textarea[type="text"], textarea[type="text"],
[type="number"], [type="number"],
.ts-control { .ts-control {
@@ -241,23 +240,6 @@ form {
} }
} }
} }
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
input[type="week"],
input[type="time"],
input[type="month"],
input[type="search"],
textarea,
select,
.ts-control {
min-height: calc(var(--nf-input-size) * 2.5);
}
input[type="text"], input[type="text"],
input[type="checkbox"], input[type="checkbox"],

View File

@@ -517,10 +517,6 @@ th {
>ul { >ul {
margin-top: 0; margin-top: 0;
} }
>input[type="checkbox"] {
padding: unset;
}
} }
td { td {

View File

@@ -2,14 +2,8 @@
<html lang="fr"> <html lang="fr">
<head> <head>
{% block head %} {% block head %}
<title>{% block title %}Association des Étudiants de l'UTBM{% endblock %}</title> <title>{% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{% block description -%}
{% trans trimmed %}
AE UTBM is a voluntary organisation run by UTBM students.
It organises student life at UTBM and manages its student facilities.
{% endtrans %}
{%- endblock %}">
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link rel="stylesheet" href="{{ static('core/base.css') }}"> <link rel="stylesheet" href="{{ static('core/base.css') }}">
<link rel="stylesheet" href="{{ static('core/style.scss') }}"> <link rel="stylesheet" href="{{ static('core/style.scss') }}">

View File

@@ -5,12 +5,16 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if page_list %}
<h3>{% trans %}Page list{% endtrans %}</h3> <h3>{% trans %}Page list{% endtrans %}</h3>
<ul> <ul>
{% for p in page_list %} {% for p in page_list %}
<li><a href="{{ p.get_absolute_url() }}">{{ p.display_name }}</a></li> <li><a href="{{ p.get_absolute_url() }}">{{ p.get_display_name() }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %}
{% trans %}There is no page in this website.{% endtrans %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -12,10 +12,7 @@
# OR WITHIN THE LOCAL FILE "LICENSE" # OR WITHIN THE LOCAL FILE "LICENSE"
# #
# #
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import F, OuterRef, Subquery
from django.db.models.functions import Coalesce
# This file contains all the views that concern the page model # This file contains all the views that concern the page model
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
@@ -46,20 +43,6 @@ class CanEditPagePropMixin(CanEditPropMixin):
class PageListView(CanViewMixin, ListView): class PageListView(CanViewMixin, ListView):
model = Page model = Page
template_name = "core/page_list.jinja" template_name = "core/page_list.jinja"
queryset = (
Page.objects.annotate(
display_name=Coalesce(
Subquery(
PageRev.objects.filter(page=OuterRef("id"))
.order_by("-date")
.values("title")[:1]
),
F("name"),
)
)
.prefetch_related("view_groups")
.select_related("parent")
)
class PageView(CanViewMixin, DetailView): class PageView(CanViewMixin, DetailView):

View File

@@ -17,8 +17,7 @@ document.addEventListener("alpine:init", () => {
this.$watch("basket", () => { this.$watch("basket", () => {
this.saveBasket(); this.saveBasket();
}); });
console.log(lastPurchaseTime);
console.log(localStorage.basketTimestamp);
// Invalidate basket if a purchase was made // Invalidate basket if a purchase was made
if (lastPurchaseTime !== null && localStorage.basketTimestamp !== undefined) { if (lastPurchaseTime !== null && localStorage.basketTimestamp !== undefined) {
if ( if (

View File

@@ -1,12 +1,8 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% block title -%} {% block title %}
{% trans %}Eboutic{% endtrans %} {% trans %}Eboutic{% endtrans %}
{%- endblock %} {% endblock %}
{% block description -%}
{% trans %}The online shop of the association.{% endtrans %}
{%- endblock %}
{% block additional_js %} {% block additional_js %}
{# This script contains the code to perform requests to manipulate the {# This script contains the code to perform requests to manipulate the

View File

@@ -2,13 +2,9 @@
{% from 'core/macros.jinja' import user_profile_link %} {% from 'core/macros.jinja' import user_profile_link %}
{% from 'forum/macros.jinja' import display_forum, display_search_bar %} {% from 'forum/macros.jinja' import display_forum, display_search_bar %}
{% block title -%} {% block title %}
{% trans %}Forum{% endtrans %} {% trans %}Forum{% endtrans %}
{%- endblock %} {% endblock %}
{% block description -%}
{% trans %}A forum dedicated to the UTBM students.{% endtrans %}
{%- endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}"> <link rel="stylesheet" href="{{ static('forum/css/forum.scss') }}">

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-13 18:32+0200\n" "POT-Creation-Date: 2025-08-23 15:30+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -174,11 +174,8 @@ msgid "You can not add the same user twice"
msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur" msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur"
#: club/forms.py #: club/forms.py
msgid "" msgid "You should specify a role"
"You cannot add other users to a club if you are not in the club board." msgstr "Vous devez choisir un rôle"
msgstr ""
"Vous ne pouvez pas ajouter d'autres utilisateurs dans un club si vous "
"ne faites pas partie de son bureau."
#: club/forms.py sas/forms.py #: club/forms.py sas/forms.py
msgid "You do not have the permission to do that" msgid "You do not have the permission to do that"
@@ -309,10 +306,6 @@ msgstr "Utilisateur non enregistré"
msgid "Club list" msgid "Club list"
msgstr "Liste des clubs" msgstr "Liste des clubs"
#: club/templates/club/club_list.jinja
msgid "The list of all clubs existing at UTBM."
msgstr "La liste de tous les clubs existants à l'UTBM"
#: club/templates/club/club_list.jinja #: club/templates/club/club_list.jinja
msgid "inactive" msgid "inactive"
msgstr "inactif" msgstr "inactif"
@@ -329,10 +322,6 @@ msgstr "Il n'y a pas de club dans ce site web."
msgid "Club members" msgid "Club members"
msgstr "Membres du club" msgstr "Membres du club"
#: club/templates/club/club_members.jinja
msgid "Add a new member"
msgstr "Ajouter un nouveau membre"
#: club/templates/club/club_members.jinja #: club/templates/club/club_members.jinja
#: club/templates/club/club_old_members.jinja #: club/templates/club/club_old_members.jinja
#: core/templates/core/user_clubs.jinja #: core/templates/core/user_clubs.jinja
@@ -357,6 +346,11 @@ msgstr "Depuis"
msgid "There are no members in this club." msgid "There are no members in this club."
msgstr "Il n'y a pas de membres dans ce club." msgstr "Il n'y a pas de membres dans ce club."
#: club/templates/club/club_members.jinja core/templates/core/file_detail.jinja
#: core/views/forms.py trombi/templates/trombi/detail.jinja
msgid "Add"
msgstr "Ajouter"
#: club/templates/club/club_old_members.jinja #: club/templates/club/club_old_members.jinja
msgid "Club old members" msgid "Club old members"
msgstr "Anciens membres du club" msgstr "Anciens membres du club"
@@ -571,12 +565,6 @@ msgstr ""
msgid "Save" msgid "Save"
msgstr "Sauver" msgstr "Sauver"
#: club/templates/club/fragments/add_member.jinja
#: core/templates/core/file_detail.jinja core/views/forms.py
#: trombi/templates/trombi/detail.jinja
msgid "Add"
msgstr "Ajouter"
#: club/templates/club/mailing.jinja #: club/templates/club/mailing.jinja
msgid "Mailing lists" msgid "Mailing lists"
msgstr "Mailing listes" msgstr "Mailing listes"
@@ -687,11 +675,6 @@ msgstr "Listes de diffusion"
msgid "Posters list" msgid "Posters list"
msgstr "Liste d'affiches" msgstr "Liste d'affiches"
#: club/views.py
#, python-format
msgid "%(user)s has been added to club."
msgstr "%(user)s a été ajouté au club."
#: com/forms.py #: com/forms.py
msgid "Format: 16:9 | Resolution: 1920x1080" msgid "Format: 16:9 | Resolution: 1920x1080"
msgstr "Format : 16:9 | Résolution : 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080"
@@ -918,7 +901,7 @@ msgid "News admin"
msgstr "Administration des nouvelles" msgstr "Administration des nouvelles"
#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja #: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja
#: com/views.py #: com/templates/com/news_list.jinja com/views.py
msgid "News" msgid "News"
msgstr "Nouvelles" msgstr "Nouvelles"
@@ -1052,7 +1035,7 @@ msgstr "Liens"
msgid "Our services" msgid "Our services"
msgstr "Nos services" msgstr "Nos services"
#: com/templates/com/news_list.jinja #: com/templates/com/news_list.jinja pedagogy/templates/pedagogy/guide.jinja
msgid "UV Guide" msgid "UV Guide"
msgstr "Guide des UVs" msgstr "Guide des UVs"
@@ -1722,12 +1705,8 @@ msgid "500, Server Error"
msgstr "500, Erreur Serveur" msgstr "500, Erreur Serveur"
#: core/templates/core/base.jinja #: core/templates/core/base.jinja
msgid "" msgid "Welcome!"
"AE UTBM is a voluntary organisation run by UTBM students. It organises " msgstr "Bienvenue !"
"student life at UTBM and manages its student facilities."
msgstr ""
"L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. "
"Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie."
#: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja #: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja
msgid "Contacts" msgid "Contacts"
@@ -2170,6 +2149,10 @@ msgstr ""
msgid "Page history" msgid "Page history"
msgstr "Historique de la page" msgstr "Historique de la page"
#: core/templates/core/page_list.jinja
msgid "There is no page in this website."
msgstr "Il n'y a pas de page sur ce site web."
#: core/templates/core/page_prop.jinja #: core/templates/core/page_prop.jinja
msgid "Page properties" msgid "Page properties"
msgstr "Propriétés de la page" msgstr "Propriétés de la page"
@@ -3836,10 +3819,6 @@ msgstr ""
msgid "Pay with Sith account" msgid "Pay with Sith account"
msgstr "Payer avec un compte AE" msgstr "Payer avec un compte AE"
#: eboutic/templates/eboutic/eboutic_main.jinja
msgid "The online shop of the association."
msgstr "La boutique en ligne de l'association."
#: eboutic/templates/eboutic/eboutic_main.jinja #: eboutic/templates/eboutic/eboutic_main.jinja
msgid "Clear" msgid "Clear"
msgstr "Vider" msgstr "Vider"
@@ -4169,10 +4148,6 @@ msgstr "Message supprimé ou non-visible."
msgid "Order by date" msgid "Order by date"
msgstr "Trier par date" msgstr "Trier par date"
#: forum/templates/forum/main.jinja
msgid "A forum dedicated to the UTBM students."
msgstr "Un forum dédié aux étudiants de l'UTBM."
#: forum/templates/forum/main.jinja #: forum/templates/forum/main.jinja
msgid "View last unread messages" msgid "View last unread messages"
msgstr "Voir les derniers messages non lus" msgstr "Voir les derniers messages non lus"
@@ -4399,14 +4374,6 @@ msgstr "signaler"
msgid "reporter" msgid "reporter"
msgstr "signalant" msgstr "signalant"
#: pedagogy/templates/pedagogy/guide.jinja
msgid "UE Guide"
msgstr "Guide des UEs"
#: pedagogy/templates/pedagogy/guide.jinja
msgid "A guide of courses available at UTBM."
msgstr "Un guide de tous les cours disponibles à l'UTBM."
#: pedagogy/templates/pedagogy/guide.jinja #: pedagogy/templates/pedagogy/guide.jinja
#, python-format #, python-format
msgid "%(display_name)s" msgid "%(display_name)s"
@@ -4699,11 +4666,6 @@ msgstr "Demande de retrait d'image"
msgid "Request removal" msgid "Request removal"
msgstr "Demander le retrait" msgstr "Demander le retrait"
#: sas/templates/sas/main.jinja
msgid "See all the photos taken during events organised by the AE."
msgstr ""
"Retrouvez toutes les photos prises lors des événements organisés par l'AE."
#: sas/templates/sas/main.jinja #: sas/templates/sas/main.jinja
msgid "You must be logged in to see the SAS." msgid "You must be logged in to see the SAS."
msgstr "Vous devez être connecté pour voir les photos." msgstr "Vous devez être connecté pour voir les photos."

8
package-lock.json generated
View File

@@ -50,7 +50,7 @@
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.2.6", "vite": "^6.3.6",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",
"vite-plugin-static-copy": "^3.1.2" "vite-plugin-static-copy": "^3.1.2"
} }
@@ -5737,9 +5737,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -35,7 +35,7 @@
"@types/jquery": "^3.5.31", "@types/jquery": "^3.5.31",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.2.6", "vite": "^6.3.6",
"vite-bundle-visualizer": "^1.2.1", "vite-bundle-visualizer": "^1.2.1",
"vite-plugin-static-copy": "^3.1.2" "vite-plugin-static-copy": "^3.1.2"
}, },

View File

@@ -2,13 +2,9 @@
{% from 'core/macros.jinja' import paginate_alpine %} {% from 'core/macros.jinja' import paginate_alpine %}
{% block title %} {% block title %}
{% trans %}UE Guide{% endtrans %} {% trans %}UV Guide{% endtrans %}
{% endblock %} {% endblock %}
{% block description -%}
{% trans %}A guide of courses available at UTBM.{% endtrans %}
{%- endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}"> <link rel="stylesheet" href="{{ static('pedagogy/css/pedagogy.scss') }}">
{% endblock %} {% endblock %}

View File

@@ -8,10 +8,6 @@
{% trans %}SAS{% endtrans %} {% trans %}SAS{% endtrans %}
{% endblock %} {% endblock %}
{% block description -%}
{% trans %}See all the photos taken during events organised by the AE.{% endtrans %}
{%- endblock %}
{% set is_sas_admin = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %} {% set is_sas_admin = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
{% from "sas/macros.jinja" import display_album %} {% from "sas/macros.jinja" import display_album %}

View File

@@ -99,10 +99,9 @@ INSTALLED_APPS = (
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.sitemaps",
"django.contrib.sites",
"django.contrib.messages", "django.contrib.messages",
"staticfiles", "staticfiles",
"django.contrib.sites",
"honeypot", "honeypot",
"django_jinja", "django_jinja",
"ninja_extra", "ninja_extra",
@@ -687,10 +686,8 @@ SITH_NOTIFICATIONS = [
# The keys are the notification names as found in SITH_NOTIFICATIONS, and the # The keys are the notification names as found in SITH_NOTIFICATIONS, and the
# values are the callback function to update the notifs. # values are the callback function to update the notifs.
# The callback must take the notif object as first and single argument. # The callback must take the notif object as first and single argument.
# If a notification is permanent but requires no post-action, set the
# callback import string as None
SITH_PERMANENT_NOTIFICATIONS = { SITH_PERMANENT_NOTIFICATIONS = {
"NEWS_MODERATION": None, "NEWS_MODERATION": "com.models.news_notification_callback",
"SAS_MODERATION": "sas.models.sas_notification_callback", "SAS_MODERATION": "sas.models.sas_notification_callback",
} }

View File

@@ -1,46 +0,0 @@
from django.conf import settings
from django.contrib.sitemaps import Sitemap
from django.db.models import OuterRef, Subquery
from django.urls import reverse
from club.models import Club
from core.models import Page, PageRev
class SithSitemap(Sitemap):
def items(self):
return [
"core:index",
"eboutic:main",
"sas:main",
"forum:main",
"club:club_list",
"election:list",
]
def location(self, item):
return reverse(item)
class PagesSitemap(Sitemap):
def items(self):
return (
Page.objects.filter(view_groups=settings.SITH_GROUP_PUBLIC_ID)
.exclude(revisions=None, _full_name__startswith="club")
.annotate(
lastmod=Subquery(
PageRev.objects.filter(page=OuterRef("pk"))
.values("date")
.order_by("-date")[:1]
)
)
.all()
)
def lastmod(self, item: Page):
return item.lastmod
class ClubSitemap(Sitemap):
def items(self):
return Club.objects.filter(is_active=True)

View File

@@ -15,24 +15,20 @@
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from django.http import Http404 from django.http import Http404
from django.urls import include, path from django.urls import include, path
from django.views.decorators.cache import cache_page
from django.views.i18n import JavaScriptCatalog from django.views.i18n import JavaScriptCatalog
from api.urls import api from api.urls import api
from sith.sitemap import ClubSitemap, PagesSitemap, SithSitemap
js_info_dict = {"packages": ("sith",)} js_info_dict = {"packages": ("sith",)}
handler403 = "core.views.forbidden" handler403 = "core.views.forbidden"
handler404 = "core.views.not_found" handler404 = "core.views.not_found"
handler500 = "core.views.internal_servor_error" handler500 = "core.views.internal_servor_error"
sitemaps = {"sith": SithSitemap, "pages": PagesSitemap, "clubs": ClubSitemap}
urlpatterns = [ urlpatterns = [
path("", include(("core.urls", "core"), namespace="core")), path("", include(("core.urls", "core"), namespace="core")),
path("sitemap.xml", cache_page(86400)(sitemap), {"sitemaps": sitemaps}),
path("api/", api.urls), path("api/", api.urls),
path("rootplace/", include(("rootplace.urls", "rootplace"), namespace="rootplace")), path("rootplace/", include(("rootplace.urls", "rootplace"), namespace="rootplace")),
path( path(