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
24 changed files with 104 additions and 303 deletions

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

@@ -171,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):

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(
if count: ~Q(dates__start_date__gt=timezone.now()), is_published=False
notifs_qs.update(viewed=False, param=str(count)) ).count()
else: if count:
notifs_qs.update(viewed=True) notif.viewed = False
notif.param = str(count)
else:
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,15 +569,9 @@ 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 return True
):
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
if isinstance(obj, User) and obj == self: if isinstance(obj, User) and obj == self:
return True return True
return self.is_owner(obj) return self.is_owner(obj)
@@ -587,18 +581,9 @@ 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. return True
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 self.can_edit(obj) return self.can_edit(obj)
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
@@ -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

@@ -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 %}
<h3>{% trans %}Page list{% endtrans %}</h3> {% if page_list %}
<ul> <h3>{% trans %}Page list{% endtrans %}</h3>
{% for p in page_list %} <ul>
<li><a href="{{ p.get_absolute_url() }}">{{ p.display_name }}</a></li> {% for p in page_list %}
{% endfor %} <li><a href="{{ p.get_absolute_url() }}">{{ p.get_display_name() }}</a></li>
</ul> {% endfor %}
</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

@@ -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

@@ -45,9 +45,8 @@ class Command(BaseCommand):
"verbosity level should be between 0 and 2 included", stacklevel=2 "verbosity level should be between 0 and 2 included", stacklevel=2
) )
if options["verbosity"] >= 2: if options["verbosity"] == 2:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logging.getLogger("django.db.backends").setLevel(logging.DEBUG)
elif options["verbosity"] == 1: elif options["verbosity"] == 1:
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
else: else:
@@ -60,3 +59,6 @@ class Command(BaseCommand):
Galaxy.objects.filter(state__isnull=True).delete() Galaxy.objects.filter(state__isnull=True).delete()
logger.info("Ruled the galaxy in {} queries.".format(len(connection.queries))) logger.info("Ruled the galaxy in {} queries.".format(len(connection.queries)))
if options["verbosity"] > 2:
for q in connection.queries:
logger.debug(q)

View File

@@ -31,14 +31,13 @@ from collections import defaultdict
from typing import NamedTuple, TypedDict from typing import NamedTuple, TypedDict
from django.db import models from django.db import models
from django.db.models import Count, Exists, F, OuterRef, Q, QuerySet from django.db.models import Count, F, Q, QuerySet
from django.utils.timezone import localdate, now from django.utils.timezone import localdate
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from club.models import Membership from club.models import Membership
from core.models import User from core.models import User
from sas.models import PeoplePictureRelation, Picture from sas.models import PeoplePictureRelation, Picture
from subscription.models import Subscription
class GalaxyStar(models.Model): class GalaxyStar(models.Model):
@@ -199,16 +198,8 @@ class Galaxy(models.Model):
cls, picture_count_threshold: int = DEFAULT_PICTURE_COUNT_THRESHOLD cls, picture_count_threshold: int = DEFAULT_PICTURE_COUNT_THRESHOLD
) -> QuerySet[User]: ) -> QuerySet[User]:
return ( return (
User.objects.filter(is_subscriber_viewable=True) User.objects.exclude(subscriptions=None)
.exclude(subscriptions=None) .annotate(pictures_count=Count("pictures"))
.annotate(
pictures_count=Count("pictures"),
is_active_in_galaxy=Exists(
Subscription.objects.filter(
member=OuterRef("id"), subscription_end__gt=now()
)
),
)
.filter(pictures_count__gt=picture_count_threshold) .filter(pictures_count__gt=picture_count_threshold)
.distinct() .distinct()
) )
@@ -299,9 +290,9 @@ class Galaxy(models.Model):
31/12/2022 (also two years, but with an offset of one year), then their 31/12/2022 (also two years, but with an offset of one year), then their
club score is 365. club score is 365.
""" """
memberships = user.memberships.values("start_date", "end_date", "club_id") memberships = user.memberships.only("start_date", "end_date", "club_id")
result = defaultdict(int) result = defaultdict(int)
today = localdate() now = localdate()
for membership in memberships: for membership in memberships:
# This is a N+1 query, but 92% of galaxy users have less than 10 memberships. # This is a N+1 query, but 92% of galaxy users have less than 10 memberships.
# Only 5 users have more than 30 memberships. # Only 5 users have more than 30 memberships.
@@ -309,23 +300,23 @@ class Galaxy(models.Model):
Membership.objects.exclude(user=user) Membership.objects.exclude(user=user)
.filter( .filter(
Q( # start2 <= start1 <= end2 Q( # start2 <= start1 <= end2
start_date__lte=membership["start_date"], start_date__lte=membership.start_date,
end_date__gte=membership["start_date"], end_date__gte=membership.start_date,
) )
| Q( # start2 <= start1 <= today | Q( # start2 <= start1 <= now
start_date__lte=membership["start_date"], end_date=None start_date__lte=membership.start_date, end_date=None
) )
| Q( # start1 <= start2 <= end2 | Q( # start1 <= start2 <= end2
start_date__gte=membership["start_date"], start_date__gte=membership.start_date,
start_date__lte=membership["end_date"] or today, start_date__lte=membership.end_date or now,
), ),
club_id=membership["club_id"], club_id=membership.club_id,
) )
.only("start_date", "end_date", "user_id") .only("start_date", "end_date", "user_id")
) )
for other in common_memberships: for other in common_memberships:
start = max(membership["start_date"], other.start_date) start = max(membership.start_date, other.start_date)
end = min(membership["end_date"] or today, other.end_date or today) end = min(membership.end_date or now, other.end_date or now)
result[other.user_id] += (end - start).days * cls.CLUBS_POINTS result[other.user_id] += (end - start).days * cls.CLUBS_POINTS
return result return result
@@ -391,22 +382,18 @@ class Galaxy(models.Model):
# this is memory expensive but prevents a lot of db hits, therefore # this is memory expensive but prevents a lot of db hits, therefore
# is far more time efficient # is far more time efficient
rulable_users_qs = self.get_rulable_users(picture_count_threshold) rulable_users = list(self.get_rulable_users(picture_count_threshold))
active_users_count = rulable_users_qs.filter(is_active_in_galaxy=True).count() rulable_users_count = len(rulable_users)
rulable_users = list(rulable_users_qs)
user1_count = 0 user1_count = 0
self.logger.info( self.logger.info(
f" {len(rulable_users)} citizens (with {active_users_count} active ones) " f"{rulable_users_count} citizen have been listed. Starting to rule."
f"have been listed. Starting to rule."
) )
self.logger.info("Creating stars for all citizen") self.logger.info("Creating stars for all citizen")
individual_scores = self.compute_individual_scores() individual_scores = self.compute_individual_scores()
GalaxyStar.objects.bulk_create( GalaxyStar.objects.bulk_create(
[ [
GalaxyStar( GalaxyStar(owner=user, galaxy=self, mass=individual_scores[user.id])
owner_id=user.id, galaxy=self, mass=individual_scores[user.id]
)
for user in rulable_users for user in rulable_users
] ]
) )
@@ -418,9 +405,9 @@ class Galaxy(models.Model):
t_global_start = time.time() t_global_start = time.time()
while len(rulable_users) > 0: while len(rulable_users) > 0:
user1 = rulable_users.pop() user1 = rulable_users.pop()
if not user1.is_active_in_galaxy:
continue
user1_count += 1 user1_count += 1
rulable_users_count2 = len(rulable_users)
star1 = stars[user1.id] star1 = stars[user1.id]
lanes = [] lanes = []
@@ -461,20 +448,17 @@ class Galaxy(models.Model):
self.logger.info("") self.logger.info("")
self.logger.info(f" Ruling of {self} ".center(60, "#")) self.logger.info(f" Ruling of {self} ".center(60, "#"))
self.logger.info( self.logger.info(
f"Progression: {user1_count}/{active_users_count} " f"Progression: {user1_count}/{rulable_users_count} "
f"citizen -- {active_users_count - user1_count} remaining" f"citizen -- {rulable_users_count - user1_count} remaining"
) )
self.logger.info(f"Speed: {global_avg_speed:.2f} citizen per second") self.logger.info(f"Speed: {global_avg_speed:.2f} citizen per second")
eta = len(rulable_users) // global_avg_speed eta = rulable_users_count2 // global_avg_speed
self.logger.info( self.logger.info(
f"ETA: {int(eta // 60 % 60)} minutes {int(eta % 60)} seconds" f"ETA: {int(eta // 60 % 60)} minutes {int(eta % 60)} seconds"
) )
self.logger.info("#" * 60) self.logger.info("#" * 60)
t_global_start = time.time() t_global_start = time.time()
count, _ = self.stars.filter(Q(lanes1=None) & Q(lanes2=None)).delete()
self.logger.info(f"{count} orphan stars have been trimmed.")
# Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy # Here, we get the IDs of the old galaxies that we'll need to delete. In normal operation, only one galaxy
# should be returned, and we can't delete it yet, as it's the one still displayed by the Sith. # should be returned, and we can't delete it yet, as it's the one still displayed by the Sith.
old_galaxies_pks = list( old_galaxies_pks = list(

View File

@@ -122,7 +122,7 @@ class TestGalaxyModel(TestCase):
self.com, self.com,
] ]
with self.assertNumQueries(38): with self.assertNumQueries(44):
while len(users) > 0: while len(users) > 0:
user1 = users.pop(0) user1 = users.pop(0)
family_scores = Galaxy.compute_user_family_score(user1) family_scores = Galaxy.compute_user_family_score(user1)
@@ -150,7 +150,7 @@ class TestGalaxyModel(TestCase):
that the number of queries to rule the galaxy is stable. that the number of queries to rule the galaxy is stable.
""" """
galaxy = Galaxy.objects.create() galaxy = Galaxy.objects.create()
with self.assertNumQueries(36): with self.assertNumQueries(39):
galaxy.rule(0) # We want everybody here galaxy.rule(0) # We want everybody here

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-01 18:18+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"
@@ -306,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"
@@ -905,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"
@@ -1039,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"
@@ -1709,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"
@@ -3827,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"
@@ -4160,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"
@@ -4390,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"
@@ -4690,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(