From d200c1e381410912736a1a8fd4c3d5494547cd76 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 28 Dec 2024 13:25:42 +0100 Subject: [PATCH 01/40] fix 500 error when accessing history of non-existing page --- core/views/page.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/views/page.py b/core/views/page.py index e33e84ba..51a0e1a5 100644 --- a/core/views/page.py +++ b/core/views/page.py @@ -64,16 +64,20 @@ class PageView(CanViewMixin, DetailView): class PageHistView(CanViewMixin, DetailView): model = Page template_name = "core/page_hist.jinja" + slug_field = "_full_name" + slug_url_kwarg = "page_name" + _cached_object: Page | None = None def dispatch(self, request, *args, **kwargs): - res = super().dispatch(request, *args, **kwargs) - if self.object.need_club_redirection: - return redirect("club:club_hist", club_id=self.object.club.id) - return res + page = self.get_object() + if page.need_club_redirection: + return redirect("club:club_hist", club_id=page.club.id) + return super().dispatch(request, *args, **kwargs) - def get_object(self): - self.page = Page.get_page_by_full_name(self.kwargs["page_name"]) - return self.page + def get_object(self, *args, **kwargs): + if not self._cached_object: + self._cached_object = super().get_object() + return self._cached_object class PageRevView(CanViewMixin, DetailView): From 6400b2c2c267c05f2df1981187af48bc89605343 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 30 Nov 2024 20:30:17 +0100 Subject: [PATCH 02/40] replace MetaGroups by proper group management --- ...012_club_board_group_club_members_group.py | 79 ++++++++ club/models.py | 183 ++++++++++-------- club/tests.py | 51 ++--- club/views.py | 5 +- core/admin.py | 4 +- core/baker_recipes.py | 3 +- core/management/commands/populate.py | 117 +++++------ core/migrations/0001_initial.py | 11 +- ..._metagroup_alter_group_options_and_more.py | 51 +++++ core/models.py | 112 ++--------- core/tests/test_core.py | 92 ++++----- core/views/forms.py | 11 +- core/views/group.py | 17 +- counter/models.py | 9 +- election/tests.py | 2 - .../commands/generate_galaxy_test_data.py | 18 +- 16 files changed, 410 insertions(+), 355 deletions(-) create mode 100644 club/migrations/0012_club_board_group_club_members_group.py create mode 100644 core/migrations/0041_delete_metagroup_alter_group_options_and_more.py diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py new file mode 100644 index 00000000..1aef164f --- /dev/null +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.16 on 2024-11-20 17:08 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models +from django.db.migrations.state import StateApps + + +def migrate_meta_groups(apps: StateApps, schema_editor): + Group = apps.get_model("core", "Group") + Club = apps.get_model("club", "Club") + + meta_groups = Group.objects.filter(is_meta=True) + clubs = list(Club.objects.all()) + for club in clubs: + club.board_group = meta_groups.get_or_create( + name=club.unix_name + settings.SITH_BOARD_SUFFIX + )[0] + club.members_group = meta_groups.get_or_create( + name=club.unix_name + settings.SITH_MEMBER_SUFFIX + )[0] + Club.objects.bulk_update(clubs, fields=["board_group", "members_group"]) + + +# steps of the migration : +# - Create a nullable field for the board group and the member group +# - Edit those new fields to make them point to currently existing meta groups +# - When this data migration is done, make the fields non-nullable +class Migration(migrations.Migration): + dependencies = [ + ("core", "0040_alter_user_options_user_user_permissions_and_more"), + ("club", "0011_auto_20180426_2013"), + ] + + operations = [ + migrations.AddField( + model_name="club", + name="board_group", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="club_board", + to="core.group", + ), + ), + migrations.AddField( + model_name="club", + name="members_group", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="club", + to="core.group", + ), + ), + migrations.RunPython( + migrate_meta_groups, reverse_code=migrations.RunPython.noop, elidable=True + ), + migrations.AlterField( + model_name="club", + name="board_group", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="club_board", + to="core.group", + ), + ), + migrations.AlterField( + model_name="club", + name="members_group", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="club", + to="core.group", + ), + ), + ] diff --git a/club/models.py b/club/models.py index 5300057d..49a711e0 100644 --- a/club/models.py +++ b/club/models.py @@ -38,7 +38,7 @@ from django.utils.functional import cached_property from django.utils.timezone import localdate from django.utils.translation import gettext_lazy as _ -from core.models import Group, MetaGroup, Notification, Page, SithFile, User +from core.models import Group, Notification, Page, SithFile, User # Create your models here. @@ -103,6 +103,12 @@ class Club(models.Model): page = models.OneToOneField( Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE ) + members_group = models.OneToOneField( + Group, related_name="club", on_delete=models.CASCADE + ) + board_group = models.OneToOneField( + Group, related_name="club_board", on_delete=models.CASCADE + ) class Meta: ordering = ["name", "unix_name"] @@ -112,23 +118,25 @@ class Club(models.Model): @transaction.atomic() def save(self, *args, **kwargs): - old = Club.objects.filter(id=self.id).first() - creation = old is None - if not creation and old.unix_name != self.unix_name: - self._change_unixname(self.unix_name) + creation = self._state.adding + if not creation and Club.objects.get(id=self.id).unix_name != self.unix_name: + self.home.name = self.unix_name + self.home.save() + if creation: + self.board_group = Group.objects.create( + name=f"{self.name} - Bureau", is_manually_manageable=False + ) + self.members_group = Group.objects.create( + name=f"{self.name} - Membres", is_manually_manageable=False + ) super().save(*args, **kwargs) if creation: - board = MetaGroup(name=self.unix_name + settings.SITH_BOARD_SUFFIX) - board.save() - member = MetaGroup(name=self.unix_name + settings.SITH_MEMBER_SUFFIX) - member.save() subscribers = Group.objects.filter( name=settings.SITH_MAIN_MEMBERS_GROUP ).first() self.make_home() - self.home.edit_groups.set([board]) - self.home.view_groups.set([member, subscribers]) - self.home.save() + self.home.edit_groups.add(self.board_group) + self.home.view_groups.add(self.members_group, subscribers) self.make_page() cache.set(f"sith_club_{self.unix_name}", self) @@ -136,7 +144,8 @@ class Club(models.Model): return reverse("club:club_view", kwargs={"club_id": self.id}) @cached_property - def president(self): + def president(self) -> Membership | None: + """Fetch the membership of the current president of this club.""" return self.members.filter( role=settings.SITH_CLUB_ROLES_ID["President"], end_date=None ).first() @@ -154,36 +163,18 @@ class Club(models.Model): def clean(self): self.check_loop() - def _change_unixname(self, old_name, new_name): - c = Club.objects.filter(unix_name=new_name).first() - if c is None: - # Update all the groups names - Group.objects.filter(name=old_name).update(name=new_name) - Group.objects.filter(name=old_name + settings.SITH_BOARD_SUFFIX).update( - name=new_name + settings.SITH_BOARD_SUFFIX - ) - Group.objects.filter(name=old_name + settings.SITH_MEMBER_SUFFIX).update( - name=new_name + settings.SITH_MEMBER_SUFFIX - ) + def make_home(self) -> None: + if self.home: + return + home_root = SithFile.objects.filter(parent=None, name="clubs").first() + root = User.objects.filter(username="root").first() + if home_root and root: + home = SithFile(parent=home_root, name=self.unix_name, owner=root) + home.save() + self.home = home + self.save() - if self.home: - self.home.name = new_name - self.home.save() - - else: - raise ValidationError(_("A club with that unix_name already exists")) - - def make_home(self): - if not self.home: - home_root = SithFile.objects.filter(parent=None, name="clubs").first() - root = User.objects.filter(username="root").first() - if home_root and root: - home = SithFile(parent=home_root, name=self.unix_name, owner=root) - home.save() - self.home = home - self.save() - - def make_page(self): + def make_page(self) -> None: root = User.objects.filter(username="root").first() if not self.page: club_root = Page.objects.filter(name=settings.SITH_CLUB_ROOT_PAGE).first() @@ -213,35 +204,32 @@ class Club(models.Model): self.page.parent = self.parent.page self.page.save(force_lock=True) - def delete(self, *args, **kwargs): + def delete(self, *args, **kwargs) -> tuple[int, dict[str, int]]: # Invalidate the cache of this club and of its memberships for membership in self.members.ongoing().select_related("user"): cache.delete(f"membership_{self.id}_{membership.user.id}") cache.delete(f"sith_club_{self.unix_name}") - super().delete(*args, **kwargs) + return super().delete(*args, **kwargs) - def get_display_name(self): + def get_display_name(self) -> str: return self.name - def is_owned_by(self, user): + def is_owned_by(self, user: User) -> bool: """Method to see if that object can be super edited by the given user.""" if user.is_anonymous: return False return user.is_board_member - def get_full_logo_url(self): - return "https://%s%s" % (settings.SITH_URL, self.logo.url) + def get_full_logo_url(self) -> str: + return f"https://{settings.SITH_URL}{self.logo.url}" - def can_be_edited_by(self, user): + def can_be_edited_by(self, user: User) -> bool: """Method to see if that object can be edited by the given user.""" return self.has_rights_in_club(user) - def can_be_viewed_by(self, user): + def can_be_viewed_by(self, user: User) -> bool: """Method to see if that object can be seen by the given user.""" - sub = User.objects.filter(pk=user.pk).first() - if sub is None: - return False - return sub.was_subscribed + return user.was_subscribed def get_membership_for(self, user: User) -> Membership | None: """Return the current membership the given user. @@ -262,12 +250,19 @@ class Club(models.Model): cache.set(f"membership_{self.id}_{user.id}", membership) return membership - def has_rights_in_club(self, user): + def has_rights_in_club(self, user: User) -> bool: m = self.get_membership_for(user) return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE class MembershipQuerySet(models.QuerySet): + @staticmethod + def _remove_from_club_groups(users: list[int], clubs: list[int]): + groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) + return User.groups.through.objects.filter( + Q(group__in=groups) & Q(user__in=users) + ).delete() + def ongoing(self) -> Self: """Filter all memberships which are not finished yet.""" return self.filter(Q(end_date=None) | Q(end_date__gt=localdate())) @@ -283,7 +278,7 @@ class MembershipQuerySet(models.QuerySet): """ return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) - def update(self, **kwargs): + def update(self, **kwargs) -> int: """Refresh the cache for the elements of the queryset. Besides that, does the same job as a regular update method. @@ -291,34 +286,48 @@ class MembershipQuerySet(models.QuerySet): Be aware that this adds a db query to retrieve the updated objects """ nb_rows = super().update(**kwargs) - if nb_rows > 0: - # if at least a row was affected, refresh the cache - for membership in self.all(): - if membership.end_date is not None: - cache.set( - f"membership_{membership.club_id}_{membership.user_id}", - "not_member", - ) - else: - cache.set( - f"membership_{membership.club_id}_{membership.user_id}", - membership, - ) + if nb_rows == 0: + # if no row was affected, no need to refresh the cache + return 0 - def delete(self): + cache_memberships = {} + to_remove = {"users": [], "clubs": []} + for membership in self.all(): + cache_key = f"membership_{membership.club_id}_{membership.user_id}" + if membership.end_date is not None and membership.end_date <= localdate(): + cache_memberships[cache_key] = "not_member" + to_remove["users"].append(membership.user_id) + to_remove["clubs"].append(membership.club_id) + else: + cache_memberships[cache_key] = membership + cache.set_many(cache_memberships) + self._remove_from_club_groups(to_remove["users"], to_remove["clubs"]) + return nb_rows + + def delete(self) -> tuple[int, dict[str, int]]: """Work just like the default Django's delete() method, but add a cache invalidation for the elements of the queryset - before the deletion. + before the deletion, + and a removal of the user from the club groups. - Be aware that this adds a db query to retrieve the deleted element. - As this first query take place before the deletion operation, - it will be performed even if the deletion fails. + Be aware that this adds a db query to retrieve the deleted element + and another to remove users from the groups. + As queries take place before the deletion operation, + they will be performed even if the deletion fails. """ ids = list(self.values_list("club_id", "user_id")) - nb_rows, _ = super().delete() + nb_rows, rows_counts = super().delete() if nb_rows > 0: - for club_id, user_id in ids: - cache.set(f"membership_{club_id}_{user_id}", "not_member") + user_ids = [i[0] for i in ids] + club_ids = [i[1] for i in ids] + self._remove_from_club_groups(user_ids, club_ids) + cache.set_many( + { + f"membership_{club_id}_{user_id}": "not_member" + for club_id, user_id in ids + } + ) + return nb_rows, rows_counts class Membership(models.Model): @@ -369,7 +378,23 @@ class Membership(models.Model): ) def save(self, *args, **kwargs): + # adding must be set before calling super().save() + # because the call will switch _state.adding from True to False + adding = self._state.adding super().save(*args, **kwargs) + if adding: + groups = [ + User.groups.through( + user_id=self.user_id, group_id=self.club.members_group_id + ) + ] + if self.role > settings.SITH_MAXIMUM_FREE_ROLE: + groups.append( + User.groups.through( + user_id=self.user_id, group_id=self.club.board_group_id + ) + ) + User.groups.through.objects.bulk_create(groups) if self.end_date is None: cache.set(f"membership_{self.club_id}_{self.user_id}", self) else: @@ -378,6 +403,10 @@ class Membership(models.Model): def get_absolute_url(self): return reverse("club:club_members", kwargs={"club_id": self.club_id}) + @property + def is_ongoing(self): + return self.end_date is None or self.end_date + def is_owned_by(self, user): """Method to see if that object can be super edited by the given user.""" if user.is_anonymous: diff --git a/club/tests.py b/club/tests.py index ae055bd0..54ea1c67 100644 --- a/club/tests.py +++ b/club/tests.py @@ -21,6 +21,7 @@ from django.urls import reverse from django.utils import timezone from django.utils.timezone import localdate, localtime, now from django.utils.translation import gettext as _ +from model_bakery import baker from club.forms import MailingForm from club.models import Club, Mailing, Membership @@ -192,10 +193,8 @@ class TestClubModel(TestClub): assert membership.end_date is None assert membership.role == role assert membership.club.get_membership_for(user) == membership - member_group = self.club.unix_name + settings.SITH_MEMBER_SUFFIX - board_group = self.club.unix_name + settings.SITH_BOARD_SUFFIX - assert user.is_in_group(name=member_group) - assert user.is_in_group(name=board_group) + assert user.is_in_group(pk=self.club.members_group_id) + assert user.is_in_group(pk=self.club.board_group_id) def assert_membership_ended_today(self, user: User): """Assert that the given user have a membership which ended today.""" @@ -474,37 +473,23 @@ class TestClubModel(TestClub): assert self.club.members.count() == nb_memberships assert membership == new_mem - def test_delete_remove_from_meta_group(self): - """Test that when a club is deleted, all its members are removed from the - associated metagroup. - """ - memberships = self.club.members.select_related("user") - users = [membership.user for membership in memberships] - meta_group = self.club.unix_name + settings.SITH_MEMBER_SUFFIX + def test_remove_from_club_group(self): + """Test that when a membership ends, the user is removed from club groups.""" + user = baker.make(User) + baker.make(Membership, user=user, club=self.club, end_date=None, role=3) + assert user.groups.contains(self.club.members_group) + assert user.groups.contains(self.club.board_group) + user.memberships.update(end_date=localdate()) + assert not user.groups.contains(self.club.members_group) + assert not user.groups.contains(self.club.board_group) - self.club.delete() - for user in users: - assert not user.is_in_group(name=meta_group) - - def test_add_to_meta_group(self): - """Test that when a membership begins, the user is added to the meta group.""" - group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX - board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX - assert not self.subscriber.is_in_group(name=group_members) - assert not self.subscriber.is_in_group(name=board_members) + def test_add_to_club_group(self): + """Test that when a membership begins, the user is added to the club group.""" + assert not self.subscriber.groups.contains(self.club.members_group) + assert not self.subscriber.groups.contains(self.club.board_group) Membership.objects.create(club=self.club, user=self.subscriber, role=3) - assert self.subscriber.is_in_group(name=group_members) - assert self.subscriber.is_in_group(name=board_members) - - def test_remove_from_meta_group(self): - """Test that when a membership ends, the user is removed from meta group.""" - group_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX - board_members = self.club.unix_name + settings.SITH_BOARD_SUFFIX - assert self.comptable.is_in_group(name=group_members) - assert self.comptable.is_in_group(name=board_members) - self.comptable.memberships.update(end_date=localtime(now())) - assert not self.comptable.is_in_group(name=group_members) - assert not self.comptable.is_in_group(name=board_members) + assert self.subscriber.groups.contains(self.club.members_group) + assert self.subscriber.groups.contains(self.club.board_group) def test_club_owner(self): """Test that a club is owned only by board members of the main club.""" diff --git a/club/views.py b/club/views.py index e1f3367e..d713a1cc 100644 --- a/club/views.py +++ b/club/views.py @@ -71,14 +71,13 @@ class ClubTabsMixin(TabedViewMixin): return self.object.get_display_name() def get_list_of_tabs(self): - tab_list = [] - tab_list.append( + tab_list = [ { "url": reverse("club:club_view", kwargs={"club_id": self.object.id}), "slug": "infos", "name": _("Infos"), } - ) + ] if self.request.user.can_view(self.object): tab_list.append( { diff --git a/core/admin.py b/core/admin.py index 367c056a..30da472a 100644 --- a/core/admin.py +++ b/core/admin.py @@ -23,8 +23,8 @@ admin.site.unregister(AuthGroup) @admin.register(Group) class GroupAdmin(admin.ModelAdmin): - list_display = ("name", "description", "is_meta") - list_filter = ("is_meta",) + list_display = ("name", "description", "is_manually_manageable") + list_filter = ("is_manually_manageable",) search_fields = ("name",) diff --git a/core/baker_recipes.py b/core/baker_recipes.py index 0abd83e0..4b873b0f 100644 --- a/core/baker_recipes.py +++ b/core/baker_recipes.py @@ -7,7 +7,7 @@ from model_bakery import seq from model_bakery.recipe import Recipe, related from club.models import Membership -from core.models import User +from core.models import Group, User from subscription.models import Subscription active_subscription = Recipe( @@ -60,5 +60,6 @@ board_user = Recipe( first_name="AE", last_name=seq("member "), memberships=related(ae_board_membership), + groups=lambda: [Group.objects.get(club_board=settings.SITH_MAIN_CLUB_ID)], ) """A user which is in the board of the AE.""" diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 0a26b4b8..9cf9c59b 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -47,7 +47,7 @@ from accounting.models import ( ) from club.models import Club, Membership from com.models import News, NewsDate, Sith, Weekmail -from core.models import Group, Page, PageRev, RealGroup, SithFile, User +from core.models import Group, Page, PageRev, SithFile, User from core.utils import resize_image from counter.models import Counter, Product, ProductType, StudentCard from election.models import Candidature, Election, ElectionList, Role @@ -143,7 +143,9 @@ class Command(BaseCommand): Counter.objects.bulk_create(counters) bar_groups = [] for bar_id, bar_name in settings.SITH_COUNTER_BARS: - group = RealGroup.objects.create(name=f"{bar_name} admin") + group = Group.objects.create( + name=f"{bar_name} admin", is_manually_manageable=True + ) bar_groups.append( Counter.edit_groups.through(counter_id=bar_id, group=group) ) @@ -366,46 +368,42 @@ Welcome to the wiki page! parent=main_club, ) - Membership.objects.bulk_create( - [ - Membership(user=skia, club=main_club, role=3), - Membership( - user=comunity, - club=bar_club, - start_date=localdate(), - role=settings.SITH_CLUB_ROLES_ID["Board member"], - ), - Membership( - user=sli, - club=troll, - role=9, - description="Padawan Troll", - start_date=localdate() - timedelta(days=17), - ), - Membership( - user=krophil, - club=troll, - role=10, - description="Maitre Troll", - start_date=localdate() - timedelta(days=200), - ), - Membership( - user=skia, - club=troll, - role=2, - description="Grand Ancien Troll", - start_date=localdate() - timedelta(days=400), - end_date=localdate() - timedelta(days=86), - ), - Membership( - user=richard, - club=troll, - role=2, - description="", - start_date=localdate() - timedelta(days=200), - end_date=localdate() - timedelta(days=100), - ), - ] + Membership.objects.create(user=skia, club=main_club, role=3) + Membership.objects.create( + user=comunity, + club=bar_club, + start_date=localdate(), + role=settings.SITH_CLUB_ROLES_ID["Board member"], + ) + Membership.objects.create( + user=sli, + club=troll, + role=9, + description="Padawan Troll", + start_date=localdate() - timedelta(days=17), + ) + Membership.objects.create( + user=krophil, + club=troll, + role=10, + description="Maitre Troll", + start_date=localdate() - timedelta(days=200), + ) + Membership.objects.create( + user=skia, + club=troll, + role=2, + description="Grand Ancien Troll", + start_date=localdate() - timedelta(days=400), + end_date=localdate() - timedelta(days=86), + ) + Membership.objects.create( + user=richard, + club=troll, + role=2, + description="", + start_date=localdate() - timedelta(days=200), + end_date=localdate() - timedelta(days=100), ) p = ProductType.objects.create(name="Bières bouteilles") @@ -594,7 +592,6 @@ Welcome to the wiki page! ) # Create an election - ae_board_group = Group.objects.get(name=settings.SITH_MAIN_BOARD_GROUP) el = Election.objects.create( title="Élection 2017", description="La roue tourne", @@ -604,7 +601,7 @@ Welcome to the wiki page! end_date="7942-06-12 10:28:45+01", ) el.view_groups.add(groups.public) - el.edit_groups.add(ae_board_group) + el.edit_groups.add(main_club.board_group) el.candidature_groups.add(groups.subscribers) el.vote_groups.add(groups.subscribers) liste = ElectionList.objects.create(title="Candidature Libre", election=el) @@ -889,7 +886,7 @@ Welcome to the wiki page! def _create_groups(self) -> PopulatedGroups: perms = Permission.objects.all() - root_group = Group.objects.create(name="Root") + root_group = Group.objects.create(name="Root", is_manually_manageable=True) root_group.permissions.add(*list(perms.values_list("pk", flat=True))) # public has no permission. # Its purpose is not to link users to permissions, @@ -911,7 +908,9 @@ Welcome to the wiki page! ) ) ) - accounting_admin = Group.objects.create(name="Accounting admin") + accounting_admin = Group.objects.create( + name="Accounting admin", is_manually_manageable=True + ) accounting_admin.permissions.add( *list( perms.filter( @@ -931,13 +930,17 @@ Welcome to the wiki page! ).values_list("pk", flat=True) ) ) - com_admin = Group.objects.create(name="Communication admin") + com_admin = Group.objects.create( + name="Communication admin", is_manually_manageable=True + ) com_admin.permissions.add( *list( perms.filter(content_type__app_label="com").values_list("pk", flat=True) ) ) - counter_admin = Group.objects.create(name="Counter admin") + counter_admin = Group.objects.create( + name="Counter admin", is_manually_manageable=True + ) counter_admin.permissions.add( *list( perms.filter( @@ -946,16 +949,20 @@ Welcome to the wiki page! ) ) ) - Group.objects.create(name="Banned from buying alcohol") - Group.objects.create(name="Banned from counters") - Group.objects.create(name="Banned to subscribe") - sas_admin = Group.objects.create(name="SAS admin") + Group.objects.create( + name="Banned from buying alcohol", is_manually_manageable=True + ) + Group.objects.create(name="Banned from counters", is_manually_manageable=True) + Group.objects.create(name="Banned to subscribe", is_manually_manageable=True) + sas_admin = Group.objects.create(name="SAS admin", is_manually_manageable=True) sas_admin.permissions.add( *list( perms.filter(content_type__app_label="sas").values_list("pk", flat=True) ) ) - forum_admin = Group.objects.create(name="Forum admin") + forum_admin = Group.objects.create( + name="Forum admin", is_manually_manageable=True + ) forum_admin.permissions.add( *list( perms.filter(content_type__app_label="forum").values_list( @@ -963,7 +970,9 @@ Welcome to the wiki page! ) ) ) - pedagogy_admin = Group.objects.create(name="Pedagogy admin") + pedagogy_admin = Group.objects.create( + name="Pedagogy admin", is_manually_manageable=True + ) pedagogy_admin.permissions.add( *list( perms.filter(content_type__app_label="pedagogy").values_list( diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 47cad8ad..23af8d06 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -563,14 +563,21 @@ class Migration(migrations.Migration): fields=[], options={"proxy": True}, bases=("core.group",), - managers=[("objects", core.models.MetaGroupManager())], + managers=[("objects", django.contrib.auth.models.GroupManager())], ), + # at first, there existed a RealGroupManager and a RealGroupManager, + # which have been since been removed. + # However, this removal broke the migrations because it caused an ImportError. + # Thus, the managers MetaGroupManager (above) and RealGroupManager (below) + # have been replaced by the base django GroupManager to fix the import. + # As those managers aren't actually used in migrations, + # this replacement doesn't break anything. migrations.CreateModel( name="RealGroup", fields=[], options={"proxy": True}, bases=("core.group",), - managers=[("objects", core.models.RealGroupManager())], + managers=[("objects", django.contrib.auth.models.GroupManager())], ), migrations.AlterUniqueTogether( name="page", unique_together={("name", "parent")} diff --git a/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py b/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py new file mode 100644 index 00000000..2a88b8c1 --- /dev/null +++ b/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.16 on 2024-11-30 13:16 + +from django.db import migrations, models +from django.db.migrations.state import StateApps +from django.db.models import F + + +def invert_is_manually_manageable(apps: StateApps, schema_editor): + """Invert `is_manually_manageable`. + + This field is a renaming of `is_meta`. + However, the meaning has been inverted : the groups + which were meta are not manually manageable and vice versa. + Thus, the value must be inverted. + """ + Group = apps.get_model("core", "Group") + Group.objects.all().update(is_manually_manageable=~F("is_manually_manageable")) + + +class Migration(migrations.Migration): + dependencies = [("core", "0040_alter_user_options_user_user_permissions_and_more")] + + operations = [ + migrations.DeleteModel( + name="MetaGroup", + ), + migrations.DeleteModel( + name="RealGroup", + ), + migrations.AlterModelOptions( + name="group", + options={}, + ), + migrations.RenameField( + model_name="group", + old_name="is_meta", + new_name="is_manually_manageable", + ), + migrations.AlterField( + model_name="group", + name="is_manually_manageable", + field=models.BooleanField( + default=False, + help_text="If False, this shouldn't be shown on group management pages", + verbose_name="Is manually manageable", + ), + ), + migrations.RunPython( + invert_is_manually_manageable, reverse_code=invert_is_manually_manageable + ), + ] diff --git a/core/models.py b/core/models.py index c8375727..945fd7d0 100644 --- a/core/models.py +++ b/core/models.py @@ -36,7 +36,6 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.models import AnonymousUser as AuthAnonymousUser from django.contrib.auth.models import Group as AuthGroup -from django.contrib.auth.models import GroupManager as AuthGroupManager from django.contrib.staticfiles.storage import staticfiles_storage from django.core import validators from django.core.cache import cache @@ -58,34 +57,17 @@ if TYPE_CHECKING: from club.models import Club -class RealGroupManager(AuthGroupManager): - def get_queryset(self): - return super().get_queryset().filter(is_meta=False) - - -class MetaGroupManager(AuthGroupManager): - def get_queryset(self): - return super().get_queryset().filter(is_meta=True) - - class Group(AuthGroup): - """Implement both RealGroups and Meta groups. + """Wrapper around django.auth.Group""" - Groups are sorted by their is_meta property - """ - - #: If False, this is a RealGroup - is_meta = models.BooleanField( - _("meta group status"), + is_manually_manageable = models.BooleanField( + _("Is manually manageable"), default=False, - help_text=_("Whether a group is a meta group or not"), + help_text=_("If False, this shouldn't be shown on group management pages"), ) #: Description of the group description = models.CharField(_("description"), max_length=60) - class Meta: - ordering = ["name"] - def get_absolute_url(self) -> str: return reverse("core:group_list") @@ -100,65 +82,6 @@ class Group(AuthGroup): cache.delete(f"sith_group_{self.name.replace(' ', '_')}") -class MetaGroup(Group): - """MetaGroups are dynamically created groups. - - Generally used with clubs where creating a club creates two groups: - - * club-SITH_BOARD_SUFFIX - * club-SITH_MEMBER_SUFFIX - """ - - #: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=False - objects = MetaGroupManager() - - class Meta: - proxy = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.is_meta = True - - @cached_property - def associated_club(self) -> Club | None: - """Return the group associated with this meta group. - - The result of this function is cached - - - Returns: - The associated club if it exists, else None - """ - from club.models import Club - - if self.name.endswith(settings.SITH_BOARD_SUFFIX): - # replace this with str.removesuffix as soon as Python - # is upgraded to 3.10 - club_name = self.name[: -len(settings.SITH_BOARD_SUFFIX)] - elif self.name.endswith(settings.SITH_MEMBER_SUFFIX): - club_name = self.name[: -len(settings.SITH_MEMBER_SUFFIX)] - else: - return None - club = cache.get(f"sith_club_{club_name}") - if club is None: - club = Club.objects.filter(unix_name=club_name).first() - cache.set(f"sith_club_{club_name}", club) - return club - - -class RealGroup(Group): - """RealGroups are created by the developer. - - Most of the time they match a number in settings to be easily used for permissions. - """ - - #: Assign a manager in a way that MetaGroup.objects only return groups with is_meta=True - objects = RealGroupManager() - - class Meta: - proxy = True - - def validate_promo(value: int) -> None: start_year = settings.SITH_SCHOOL_START_YEAR delta = (localdate() + timedelta(days=180)).year - start_year @@ -204,8 +127,8 @@ def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None else: group = Group.objects.filter(name=name).first() if group is not None: - cache.set(f"sith_group_{group.id}", group) - cache.set(f"sith_group_{group.name.replace(' ', '_')}", group) + name = group.name.replace(" ", "_") + cache.set_many({f"sith_group_{group.id}": group, f"sith_group_{name}": group}) else: cache.set(f"sith_group_{pk_or_name}", "not_found") return group @@ -438,18 +361,6 @@ class User(AbstractUser): return self.was_subscribed if group.id == settings.SITH_GROUP_ROOT_ID: return self.is_root - if group.is_meta: - # check if this group is associated with a club - group.__class__ = MetaGroup - club = group.associated_club - if club is None: - return False - membership = club.get_membership_for(self) - if membership is None: - return False - if group.name.endswith(settings.SITH_MEMBER_SUFFIX): - return True - return membership.role > settings.SITH_MAXIMUM_FREE_ROLE return group in self.cached_groups @property @@ -474,12 +385,11 @@ class User(AbstractUser): return any(g.id == root_id for g in self.cached_groups) @cached_property - def is_board_member(self): - main_club = settings.SITH_MAIN_CLUB["unix_name"] - return self.is_in_group(name=main_club + settings.SITH_BOARD_SUFFIX) + def is_board_member(self) -> bool: + return self.groups.filter(club_board=settings.SITH_MAIN_CLUB_ID).exists() @cached_property - def can_read_subscription_history(self): + def can_read_subscription_history(self) -> bool: if self.is_root or self.is_board_member: return True @@ -943,7 +853,7 @@ class SithFile(models.Model): param="1", ).save() - def is_owned_by(self, user): + def is_owned_by(self, user: User) -> bool: if user.is_anonymous: return False if user.is_root: @@ -958,7 +868,7 @@ class SithFile(models.Model): return True return user.id == self.owner_id - def can_be_viewed_by(self, user): + def can_be_viewed_by(self, user: User) -> bool: if hasattr(self, "profile_of"): return user.can_view(self.profile_of) if hasattr(self, "avatar_of"): diff --git a/core/tests/test_core.py b/core/tests/test_core.py index a33a8705..a152b579 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -18,6 +18,7 @@ from smtplib import SMTPException import freezegun import pytest +from django.contrib.auth.hashers import make_password from django.core import mail from django.core.cache import cache from django.core.mail import EmailMessage @@ -30,7 +31,7 @@ from model_bakery import baker from pytest_django.asserts import assertInHTML, assertRedirects from antispam.models import ToxicDomain -from club.models import Membership +from club.models import Club, Membership from core.markdown import markdown from core.models import AnonymousUser, Group, Page, User from core.utils import get_semester_code, get_start_of_semester @@ -145,7 +146,7 @@ class TestUserRegistration: class TestUserLogin: @pytest.fixture() def user(self) -> User: - return User.objects.first() + return baker.make(User, password=make_password("plop")) def test_login_fail(self, client, user): """Should not login a user correctly.""" @@ -349,14 +350,9 @@ class TestUserIsInGroup(TestCase): @classmethod def setUpTestData(cls): - from club.models import Club - cls.root_group = Group.objects.get(name="Root") - cls.public = Group.objects.get(name="Public") - cls.skia = User.objects.get(username="skia") - cls.toto = User.objects.create( - username="toto", first_name="a", last_name="b", email="a.b@toto.fr" - ) + cls.public_group = Group.objects.get(name="Public") + cls.public_user = baker.make(User) cls.subscribers = Group.objects.get(name="Subscribers") cls.old_subscribers = Group.objects.get(name="Old subscribers") cls.accounting_admin = Group.objects.get(name="Accounting admin") @@ -366,22 +362,12 @@ class TestUserIsInGroup(TestCase): cls.banned_counters = Group.objects.get(name="Banned from counters") cls.banned_subscription = Group.objects.get(name="Banned to subscribe") cls.sas_admin = Group.objects.get(name="SAS admin") - cls.club = Club.objects.create( - name="Fake Club", - unix_name="fake-club", - address="Fake address", - ) + cls.club = baker.make(Club) cls.main_club = Club.objects.get(id=1) def assert_in_public_group(self, user): - assert user.is_in_group(pk=self.public.id) - assert user.is_in_group(name=self.public.name) - - def assert_in_club_metagroups(self, user, club): - meta_groups_board = club.unix_name + settings.SITH_BOARD_SUFFIX - meta_groups_members = club.unix_name + settings.SITH_MEMBER_SUFFIX - assert user.is_in_group(name=meta_groups_board) is False - assert user.is_in_group(name=meta_groups_members) is False + assert user.is_in_group(pk=self.public_group.id) + assert user.is_in_group(name=self.public_group.name) def assert_only_in_public_group(self, user): self.assert_in_public_group(user) @@ -392,13 +378,11 @@ class TestUserIsInGroup(TestCase): self.sas_admin, self.subscribers, self.old_subscribers, + self.club.members_group, + self.club.board_group, ): assert not user.is_in_group(pk=group.pk) assert not user.is_in_group(name=group.name) - meta_groups_board = self.club.unix_name + settings.SITH_BOARD_SUFFIX - meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX - assert user.is_in_group(name=meta_groups_board) is False - assert user.is_in_group(name=meta_groups_members) is False def test_anonymous_user(self): """Test that anonymous users are only in the public group.""" @@ -407,80 +391,80 @@ class TestUserIsInGroup(TestCase): def test_not_subscribed_user(self): """Test that users who never subscribed are only in the public group.""" - self.assert_only_in_public_group(self.toto) + self.assert_only_in_public_group(self.public_user) def test_wrong_parameter_fail(self): """Test that when neither the pk nor the name argument is given, the function raises a ValueError. """ with self.assertRaises(ValueError): - self.toto.is_in_group() + self.public_user.is_in_group() def test_number_queries(self): """Test that the number of db queries is stable and that less queries are made when making a new call. """ # make sure Skia is in at least one group - self.skia.groups.add(Group.objects.first().pk) - skia_groups = self.skia.groups.all() + group_in = baker.make(Group) + self.public_user.groups.add(group_in) - group_in = skia_groups.first() cache.clear() # Test when the user is in the group with self.assertNumQueries(2): - self.skia.is_in_group(pk=group_in.id) + self.public_user.is_in_group(pk=group_in.id) with self.assertNumQueries(0): - self.skia.is_in_group(pk=group_in.id) + self.public_user.is_in_group(pk=group_in.id) - ids = skia_groups.values_list("pk", flat=True) - group_not_in = Group.objects.exclude(pk__in=ids).first() + group_not_in = baker.make(Group) cache.clear() # Test when the user is not in the group with self.assertNumQueries(2): - self.skia.is_in_group(pk=group_not_in.id) + self.public_user.is_in_group(pk=group_not_in.id) with self.assertNumQueries(0): - self.skia.is_in_group(pk=group_not_in.id) + self.public_user.is_in_group(pk=group_not_in.id) def test_cache_properly_cleared_membership(self): """Test that when the membership of a user end, the cache is properly invalidated. """ - membership = Membership.objects.create( - club=self.club, user=self.toto, end_date=None - ) - meta_groups_members = self.club.unix_name + settings.SITH_MEMBER_SUFFIX + membership = baker.make(Membership, club=self.club, user=self.public_user) cache.clear() - assert self.toto.is_in_group(name=meta_groups_members) is True - assert membership == cache.get(f"membership_{self.club.id}_{self.toto.id}") + self.club.get_membership_for(self.public_user) # this should populate the cache + assert membership == cache.get( + f"membership_{self.club.id}_{self.public_user.id}" + ) membership.end_date = now() - timedelta(minutes=5) membership.save() - cached_membership = cache.get(f"membership_{self.club.id}_{self.toto.id}") + cached_membership = cache.get( + f"membership_{self.club.id}_{self.public_user.id}" + ) assert cached_membership == "not_member" - assert self.toto.is_in_group(name=meta_groups_members) is False def test_cache_properly_cleared_group(self): """Test that when a user is removed from a group, the is_in_group_method return False when calling it again. """ # testing with pk - self.toto.groups.add(self.com_admin.pk) - assert self.toto.is_in_group(pk=self.com_admin.pk) is True + self.public_user.groups.add(self.com_admin.pk) + assert self.public_user.is_in_group(pk=self.com_admin.pk) is True - self.toto.groups.remove(self.com_admin.pk) - assert self.toto.is_in_group(pk=self.com_admin.pk) is False + self.public_user.groups.remove(self.com_admin.pk) + assert self.public_user.is_in_group(pk=self.com_admin.pk) is False # testing with name - self.toto.groups.add(self.sas_admin.pk) - assert self.toto.is_in_group(name="SAS admin") is True + self.public_user.groups.add(self.sas_admin.pk) + assert self.public_user.is_in_group(name="SAS admin") is True - self.toto.groups.remove(self.sas_admin.pk) - assert self.toto.is_in_group(name="SAS admin") is False + self.public_user.groups.remove(self.sas_admin.pk) + assert self.public_user.is_in_group(name="SAS admin") is False def test_not_existing_group(self): """Test that searching for a not existing group returns False. """ - assert self.skia.is_in_group(name="This doesn't exist") is False + user = baker.make(User) + user.groups.set(list(Group.objects.all())) + assert not user.is_in_group(name="This doesn't exist") class TestDateUtils(TestCase): diff --git a/core/views/forms.py b/core/views/forms.py index bda33ec0..5dbf8f3e 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -32,19 +32,14 @@ from django.contrib.staticfiles.management.commands.collectstatic import ( ) from django.core.exceptions import ValidationError from django.db import transaction -from django.forms import ( - CheckboxSelectMultiple, - DateInput, - DateTimeInput, - TextInput, -) +from django.forms import CheckboxSelectMultiple, DateInput, DateTimeInput, TextInput from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from phonenumber_field.widgets import RegionalPhoneNumberWidget from PIL import Image from antispam.forms import AntiSpamEmailField -from core.models import Gift, Page, RealGroup, SithFile, User +from core.models import Gift, Group, Page, SithFile, User from core.utils import resize_image from core.views.widgets.select import ( AutoCompleteSelect, @@ -293,7 +288,7 @@ class UserGroupsForm(forms.ModelForm): required_css_class = "required" groups = forms.ModelMultipleChoiceField( - queryset=RealGroup.objects.all(), + queryset=Group.objects.filter(is_manually_manageable=True), widget=CheckboxSelectMultiple, label=_("Groups"), required=False, diff --git a/core/views/group.py b/core/views/group.py index b6e77b54..978fe686 100644 --- a/core/views/group.py +++ b/core/views/group.py @@ -21,7 +21,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView from django.views.generic.edit import CreateView, DeleteView, UpdateView -from core.models import RealGroup, User +from core.models import Group, User from core.views import CanCreateMixin, CanEditMixin, DetailFormView from core.views.widgets.select import AutoCompleteSelectMultipleUser @@ -57,7 +57,8 @@ class EditMembersForm(forms.Form): class GroupListView(CanEditMixin, ListView): """Displays the Group list.""" - model = RealGroup + model = Group + queryset = Group.objects.filter(is_manually_manageable=True) ordering = ["name"] template_name = "core/group_list.jinja" @@ -65,7 +66,8 @@ class GroupListView(CanEditMixin, ListView): class GroupEditView(CanEditMixin, UpdateView): """Edit infos of a Group.""" - model = RealGroup + model = Group + queryset = Group.objects.filter(is_manually_manageable=True) pk_url_kwarg = "group_id" template_name = "core/group_edit.jinja" fields = ["name", "description"] @@ -74,7 +76,8 @@ class GroupEditView(CanEditMixin, UpdateView): class GroupCreateView(CanCreateMixin, CreateView): """Add a new Group.""" - model = RealGroup + model = Group + queryset = Group.objects.filter(is_manually_manageable=True) template_name = "core/create.jinja" fields = ["name", "description"] @@ -84,7 +87,8 @@ class GroupTemplateView(CanEditMixin, DetailFormView): Allow adding and removing users from it. """ - model = RealGroup + model = Group + queryset = Group.objects.filter(is_manually_manageable=True) form_class = EditMembersForm pk_url_kwarg = "group_id" template_name = "core/group_detail.jinja" @@ -118,7 +122,8 @@ class GroupTemplateView(CanEditMixin, DetailFormView): class GroupDeleteView(CanEditMixin, DeleteView): """Delete a Group.""" - model = RealGroup + model = Group + queryset = Group.objects.filter(is_manually_manageable=True) pk_url_kwarg = "group_id" template_name = "core/delete_confirm.jinja" success_url = reverse_lazy("core:group_list") diff --git a/counter/models.py b/counter/models.py index 6668e520..9fade666 100644 --- a/counter/models.py +++ b/counter/models.py @@ -532,9 +532,12 @@ class Counter(models.Model): return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) def can_be_viewed_by(self, user: User) -> bool: - if self.type == "BAR": - return True - return user.is_board_member or user in self.sellers.all() + return ( + self.type == "BAR" + or user.is_root + or user.is_in_group(pk=self.club.board_group_id) + or user in self.sellers.all() + ) def gen_token(self) -> None: """Generate a new token for this counter.""" diff --git a/election/tests.py b/election/tests.py index 5152cc3d..f4cc380e 100644 --- a/election/tests.py +++ b/election/tests.py @@ -11,8 +11,6 @@ class TestElection(TestCase): def setUpTestData(cls): cls.election = Election.objects.first() cls.public_group = Group.objects.get(id=settings.SITH_GROUP_PUBLIC_ID) - cls.subscriber_group = Group.objects.get(name=settings.SITH_MAIN_MEMBERS_GROUP) - cls.ae_board_group = Group.objects.get(name=settings.SITH_MAIN_BOARD_GROUP) cls.sli = User.objects.get(username="sli") cls.subscriber = User.objects.get(username="subscriber") cls.public = User.objects.get(username="public") diff --git a/galaxy/management/commands/generate_galaxy_test_data.py b/galaxy/management/commands/generate_galaxy_test_data.py index 39334ddd..2c3ab7c8 100644 --- a/galaxy/management/commands/generate_galaxy_test_data.py +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -118,18 +118,18 @@ class Command(BaseCommand): self.make_important_citizen(u) def make_clubs(self): - """Create all the clubs (:class:`club.models.Club`). + """Create all the clubs [club.models.Club][]. After creation, the clubs are stored in `self.clubs` for fast access later. - Don't create the meta groups (:class:`core.models.MetaGroup`) - nor the pages of the clubs (:class:`core.models.Page`). + Don't create the groups associated to the clubs + nor the pages of the clubs ([core.models.Page][]). """ - self.clubs = [] - for i in range(self.NB_CLUBS): - self.clubs.append(Club(unix_name=f"galaxy-club-{i}", name=f"club-{i}")) - # We don't need to create corresponding groups here, as the Galaxy doesn't care about them - Club.objects.bulk_create(self.clubs) - self.clubs = list(Club.objects.filter(unix_name__startswith="galaxy-").all()) + self.clubs = Club.objects.bulk_create( + [ + Club(unix_name=f"galaxy-club-{i}", name=f"club-{i}") + for i in range(self.NB_CLUBS) + ] + ) def make_users(self): """Create all the users and store them in `self.users` for fast access later. From 407cfbe02b82ab7144a41fa033de72bbabc80468 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 30 Nov 2024 22:47:47 +0100 Subject: [PATCH 03/40] update docs --- docs/tutorial/groups.md | 194 +++++++++++++++++++++++++++++++--------- docs/tutorial/perms.md | 2 +- 2 files changed, 153 insertions(+), 43 deletions(-) diff --git a/docs/tutorial/groups.md b/docs/tutorial/groups.md index b8603d16..1b3677a5 100644 --- a/docs/tutorial/groups.md +++ b/docs/tutorial/groups.md @@ -1,54 +1,157 @@ -Il existe deux types de groupes sur le site AE : +## Un peu d'histoire + +Par défaut, Django met à disposition un modèle `Group`, +lié par clef étrangère au modèle `User`. +Pour créer un système de gestion des groupes qui semblait plus +approprié aux développeurs initiaux, un nouveau +modèle [core.models.Group][] +a été crée, et la relation de clef étrangère a été modifiée +pour lier [core.models.User][] à ce dernier. + +L'ancien modèle `Group` était implicitement +divisé en deux catégories : + +- les *méta-groupes* : groupes liés aux clubs et créés à la volée. + Ces groupes n'étaient liés par clef étrangère à aucun utilisateur. + Ils étaient récupérés à partir de leur nom uniquement + et étaient plus une indirection pour désigner l'appartenance à un club + que des groupes à proprement parler. +- les *groupes réels* : groupes créés à la main + et souvent hardcodés dans la configuration. + +Cependant, ce nouveau système s'éloignait trop du cadre de Django +et a fini par devenir une gêne. +La vérification des droits lors des opérations est devenue +une opération complexe et coûteuse en temps. + +La gestion des groupes a donc été modifiée pour recoller un +peu plus au cadre de Django. +Toutefois, il n'a pas été tenté de revenir à 100% +sur l'architecture prônée par Django. + +D'une part, cela représentait un risque pour le succès de l'application +de la migration sur la base de données de production. + +D'autre part, si une autre architecture a été tentée au début, +ce n'était pas sans raison : +ce que nous voulons modéliser sur le site AE n'est pas +complètement modélisable avec ce qu'offre Django. +Il faut donc bien garder une surcouche au-dessus de l'authentification +de Django. +Tout le défi est de réussir à maintenir cette surcouche aussi fine +que possible sans limiter ce que nous voulons faire. + +## Représentation en base de données + +Le modèle [core.models.Group][] a donc été légèrement remanié +et la distinction entre groupes méta et groupes réels a été plus ou moins +supprimée. +La liaison de clef étrangère se fait toujours entre [core.models.User][] +et [core.models.Group][]. + +Cependant, il y a une subtilité. +Depuis le début, le modèle `Group` de django n'a jamais disparu. +En effet, lorsqu'un modèle hérite d'un modèle qui n'est pas +abstrait, Django garde les deux tables et les lie +par une clef étrangère unique de clef primaire à clef primaire +(pour plus de détail, lire +[la doc de django sur l'héritage de modèle](https://docs.djangoproject.com/fr/stable/topics/db/models/#model-inheritance)) + +L'organisation réelle de notre système de groupes +est donc la suivante : + + +```mermaid +--- +title: Représentation des groupes +--- +erDiagram + core_user }o..o{ core_group: core_user_groups + auth_group }o..o{ auth_permission: auth_group_permissions + core_group ||--|| auth_group: "" + core_user }o..o{ auth_permission :"core_user_user_permissions" + + core_user { + int id PK + string username + string email + string first_name + etc etc + } + core_group { + int group_ptr_id PK,FK + string description + bool is_manually_manageable + } + auth_group { + int id PK + name string + } + auth_permission { + int id PK + string name + } +``` + +Cette organisation, rajoute une certaine complexité, +mais celle-ci est presque entièrement gérée par django, +ce qui fait que la gestion n'est pas tellement plus compliquée +du point de vue du développeur. + +Chaque fois qu'un queryset implique notre `Group` +ou le `Group` de django, l'autre modèle est automatiquement +ajouté à la requête par jointure. +De cette façon, on peut manipuler l'un ou l'autre, +sans même se rendre que les tables sont dans des tables séparées. + +Par exemple : + +=== "python" + + ```python + from core.models import Group + + Group.objects.all() + ``` + +=== "SQL généré" + + ```sql + SELECT "auth_group"."id", + "auth_group"."name", + "core_group"."group_ptr_id", + "core_group"."is_manually_manageable", + "core_group"."description" + FROM "core_group" + INNER JOIN "auth_group" ON ("core_group"."group_ptr_id" = "auth_group"."id") + ``` -- l'un se base sur des groupes enregistrés en base de données pendant le développement, - c'est le système de groupes réels. -- L'autre est plus dynamique et comprend tous les groupes générés - pendant l'exécution et l'utilisation du programme. - Cela correspond généralement aux groupes liés aux clubs. - Ce sont les méta-groupes. ## La définition d'un groupe -Les deux types de groupes sont stockés dans la même table -en base de données, et ne sont différenciés que par un attribut `is_meta`. +Un groupe est constitué des informations suivantes : -### Les groupes réels +- son nom : `name` +- sa description : `description` (optionnelle) +- si on autorise sa gestion par les utilisateurs du site : `is_manually_manageable` -Pour plus différencier l'utilisation de ces deux types de groupe, -il a été créé une classe proxy -(c'est-à-dire qu'elle ne correspond pas à une vraie table en base de donnée) -qui encapsule leur utilisation. -`RealGroup` peut être utilisé pour créer des groupes réels dans le code -et pour faire une recherche sur ceux-ci -(dans le cadre d'une vérification de permissions par exemple). +Si un groupe est gérable manuellement, alors les administrateurs du site +auront le droit d'assigner des utilisateurs à ce groupe depuis l'interface dédiée. -### Les méta-groupes +S'il n'est pas gérable manuellement, on cache aux utilisateurs du site +la gestion des membres de ce groupe. +La gestion se fait alors uniquement "sous le capot", +de manière automatique lors de certains évènements. +Par exemple, lorsqu'un utilisateur rejoint un club, +il est automatiquement ajouté au groupe des membres +du club. +Lorsqu'il quitte le club, il est retiré du groupe. -Les méta-groupes, comme expliqué précédemment, -sont utilisés dans les contextes où il est nécessaire de créer un groupe dynamiquement. -Les objets `MetaGroup`, bien que dynamiques, doivent tout de même s'enregistrer -en base de données comme des vrais groupes afin de pouvoir être affectés -dans les permissions d'autres objets, comme un forum ou une page de wiki par exemple. -C'est principalement utilisé au travers des clubs, -qui génèrent automatiquement deux groupes à leur création : +## Les principaux groupes utilisés -- club-bureau : contient tous les membres d'un club **au dessus** - du grade défini dans `settings.SITH_MAXIMUM_FREE_ROLE`. -- club-membres : contient tous les membres d'un club - **en dessous** du grade défini dans `settings.SITH_MAXIMUM_FREE_ROLE`. - - -## Les groupes réels utilisés - -Les groupes réels que l'on utilise dans le site sont les suivants : - -Groupes gérés automatiquement par le site : - -- `Public` : tous les utilisateurs du site -- `Subscribers` : tous les cotisants du site -- `Old subscribers` : tous les anciens cotisants - -Groupes gérés par les administrateurs (à appliquer à la main sur un utilisateur) : +Les groupes les plus notables gérables par les administrateurs du site sont : - `Root` : administrateur global du site - `Accounting admin` : les administrateurs de la comptabilité @@ -62,3 +165,10 @@ Groupes gérés par les administrateurs (à appliquer à la main sur un utilisat - `Banned to subscribe` : les utilisateurs interdits de cotisation +En plus de ces groupes, on peut noter : + +- `Public` : tous les utilisateurs du site +- `Subscribers` : tous les cotisants du site +- `Old subscribers` : tous les anciens cotisants + + diff --git a/docs/tutorial/perms.md b/docs/tutorial/perms.md index 3377b808..c78292ab 100644 --- a/docs/tutorial/perms.md +++ b/docs/tutorial/perms.md @@ -4,7 +4,7 @@ Le fonctionnement de l'AE ne permet pas d'utiliser le système de permissions intégré à Django tel quel. Lors de la conception du Sith, ce qui paraissait le plus simple à l'époque était de concevoir un système maison afin de se calquer -sur ce que faisais l'ancien site. +sur ce que faisait l'ancien site. ### Protéger un modèle From 9a72c5eb723f4f01495c88860db60bbf840e4bd0 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 30 Nov 2024 23:03:49 +0100 Subject: [PATCH 04/40] fix galaxy tests --- docs/tutorial/groups.md | 8 ++++++++ .../commands/generate_galaxy_test_data.py | 14 +++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/groups.md b/docs/tutorial/groups.md index 1b3677a5..fbb58aab 100644 --- a/docs/tutorial/groups.md +++ b/docs/tutorial/groups.md @@ -128,6 +128,14 @@ Par exemple : INNER JOIN "auth_group" ON ("core_group"."group_ptr_id" = "auth_group"."id") ``` +!!!warning + + Django réussit à abstraire assez bien la logique relationnelle. + Cependant, gardez bien en mémoire que ce n'est pas quelque chose + de magique et que cette manière de faire a des limitations. + Par exemple, il devient impossible de `bulk_create` + des groupes. + ## La définition d'un groupe diff --git a/galaxy/management/commands/generate_galaxy_test_data.py b/galaxy/management/commands/generate_galaxy_test_data.py index 2c3ab7c8..d0dea4a5 100644 --- a/galaxy/management/commands/generate_galaxy_test_data.py +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -121,12 +121,20 @@ class Command(BaseCommand): """Create all the clubs [club.models.Club][]. After creation, the clubs are stored in `self.clubs` for fast access later. - Don't create the groups associated to the clubs - nor the pages of the clubs ([core.models.Page][]). + Don't create the pages of the clubs ([core.models.Page][]). """ + # dummy groups. + # the galaxy doesn't care about the club groups, + # but it's necessary to add them nonetheless in order + # not to break the integrity constraints self.clubs = Club.objects.bulk_create( [ - Club(unix_name=f"galaxy-club-{i}", name=f"club-{i}") + Club( + unix_name=f"galaxy-club-{i}", + name=f"club-{i}", + board_group=Group.objects.create(name=f"board {i}"), + members_group=Group.objects.create(name=f"members {i}"), + ) for i in range(self.NB_CLUBS) ] ) From d380668c0f2cb26a4b3ea8dfac23a2c15f318f89 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 1 Dec 2024 20:47:14 +0100 Subject: [PATCH 05/40] Move users to the club groups in the migration --- ...012_club_board_group_club_members_group.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py index 1aef164f..67e79f58 100644 --- a/club/migrations/0012_club_board_group_club_members_group.py +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -4,9 +4,28 @@ import django.db.models.deletion from django.conf import settings from django.db import migrations, models from django.db.migrations.state import StateApps +from django.db.models import Q +from django.utils.timezone import localdate def migrate_meta_groups(apps: StateApps, schema_editor): + """Attach the existing meta groups to the clubs. + + Until now, the meta groups were not attached to the clubs, + nor to the users. + This creates actual foreign relationships between the clubs + and theirs groups and the users and theirs groups. + + Warnings: + When the meta groups associated with the clubs aren't found, + they are created. + Thus the migration shouldn't fail, and all the clubs will + have their groups. + However, there will probably be some groups that have + not been found but exist nonetheless, + so there will be duplicates and dangling groups. + There must be a manual cleanup after this migration. + """ Group = apps.get_model("core", "Group") Club = apps.get_model("club", "Club") @@ -19,7 +38,18 @@ def migrate_meta_groups(apps: StateApps, schema_editor): club.members_group = meta_groups.get_or_create( name=club.unix_name + settings.SITH_MEMBER_SUFFIX )[0] - Club.objects.bulk_update(clubs, fields=["board_group", "members_group"]) + club.save() + club.refresh_from_db() + memberships = club.members.filter( + Q(end_date=None) | Q(end_date__gt=localdate()) + ).select_related("user") + club.members_group.users.set([m.user for m in memberships]) + club.board_group.users.set( + [ + m.user + for m in memberships.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) + ] + ) # steps of the migration : From 0ae1e850f498400aaa151f795b7b4d943d7b39cd Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 2 Dec 2024 19:53:24 +0100 Subject: [PATCH 06/40] improve admin --- club/admin.py | 11 +++++++++++ core/admin.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/club/admin.py b/club/admin.py index c2444c17..2515e208 100644 --- a/club/admin.py +++ b/club/admin.py @@ -20,6 +20,17 @@ from club.models import Club, Membership @admin.register(Club) class ClubAdmin(admin.ModelAdmin): list_display = ("name", "unix_name", "parent", "is_active") + search_fields = ("name", "unix_name") + autocomplete_fields = ( + "parent", + "board_group", + "members_group", + "view_groups", + "edit_groups", + "owner_group", + "home", + "page", + ) @admin.register(Membership) diff --git a/core/admin.py b/core/admin.py index 30da472a..601ba636 100644 --- a/core/admin.py +++ b/core/admin.py @@ -15,6 +15,7 @@ from django.contrib import admin from django.contrib.auth.models import Group as AuthGroup +from django.contrib.auth.models import Permission from core.models import Group, OperationLog, Page, SithFile, User @@ -26,6 +27,7 @@ class GroupAdmin(admin.ModelAdmin): list_display = ("name", "description", "is_manually_manageable") list_filter = ("is_manually_manageable",) search_fields = ("name",) + autocomplete_fields = ("permissions",) @admin.register(User) @@ -37,10 +39,17 @@ class UserAdmin(admin.ModelAdmin): "profile_pict", "avatar_pict", "scrub_pict", + "user_permissions", + "groups", ) search_fields = ["first_name", "last_name", "username"] +@admin.register(Permission) +class PermissionAdmin(admin.ModelAdmin): + search_fields = ("codename",) + + @admin.register(Page) class PageAdmin(admin.ModelAdmin): list_display = ("name", "_full_name", "owner_group") From 1e29ae41719fcd28ec27efe8264443f04094b0cc Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 3 Dec 2024 17:50:14 +0100 Subject: [PATCH 07/40] fixes on club group attribution --- ...012_club_board_group_club_members_group.py | 8 + club/models.py | 184 +++++++++++++----- club/tests.py | 48 ++++- core/management/commands/populate_more.py | 3 +- 4 files changed, 188 insertions(+), 55 deletions(-) diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py index 67e79f58..41520c22 100644 --- a/club/migrations/0012_club_board_group_club_members_group.py +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -1,6 +1,7 @@ # Generated by Django 4.2.16 on 2024-11-20 17:08 import django.db.models.deletion +import django.db.models.functions.datetime from django.conf import settings from django.db import migrations, models from django.db.migrations.state import StateApps @@ -106,4 +107,11 @@ class Migration(migrations.Migration): to="core.group", ), ), + migrations.AddConstraint( + model_name="membership", + constraint=models.CheckConstraint( + check=models.Q(("end_date__gte", models.F("start_date"))), + name="end_after_start", + ), + ), ] diff --git a/club/models.py b/club/models.py index 49a711e0..2b74155f 100644 --- a/club/models.py +++ b/club/models.py @@ -23,7 +23,7 @@ # from __future__ import annotations -from typing import Self +from typing import Iterable, Self from django.conf import settings from django.core import validators @@ -31,7 +31,7 @@ from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import RegexValidator, validate_email from django.db import models, transaction -from django.db.models import Exists, OuterRef, Q +from django.db.models import Exists, F, OuterRef, Q from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -256,13 +256,6 @@ class Club(models.Model): class MembershipQuerySet(models.QuerySet): - @staticmethod - def _remove_from_club_groups(users: list[int], clubs: list[int]): - groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) - return User.groups.through.objects.filter( - Q(group__in=groups) & Q(user__in=users) - ).delete() - def ongoing(self) -> Self: """Filter all memberships which are not finished yet.""" return self.filter(Q(end_date=None) | Q(end_date__gt=localdate())) @@ -279,11 +272,16 @@ class MembershipQuerySet(models.QuerySet): return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) def update(self, **kwargs) -> int: - """Refresh the cache for the elements of the queryset. + """Refresh the cache and edit group ownership. - Besides that, does the same job as a regular update method. + Update the cache, when necessary, remove + users from club groups they are no more in + and add them in the club groups they should be in. - Be aware that this adds a db query to retrieve the updated objects + Be aware that this adds three db queries : + one to retrieve the updated memberships, + one to perform group removal and one to perform + group attribution. """ nb_rows = super().update(**kwargs) if nb_rows == 0: @@ -291,17 +289,18 @@ class MembershipQuerySet(models.QuerySet): return 0 cache_memberships = {} - to_remove = {"users": [], "clubs": []} - for membership in self.all(): - cache_key = f"membership_{membership.club_id}_{membership.user_id}" - if membership.end_date is not None and membership.end_date <= localdate(): - cache_memberships[cache_key] = "not_member" - to_remove["users"].append(membership.user_id) - to_remove["clubs"].append(membership.club_id) + memberships = set(self.select_related("club")) + # delete all User-Group relations and recreate the necessary ones + # It's more concise to write and more reliable + Membership._remove_club_groups(memberships) + Membership._add_club_groups(memberships) + for member in memberships: + cache_key = f"membership_{member.club_id}_{member.user_id}" + if member.end_date is None: + cache_memberships[cache_key] = member else: - cache_memberships[cache_key] = membership + cache_memberships[cache_key] = "not_member" cache.set_many(cache_memberships) - self._remove_from_club_groups(to_remove["users"], to_remove["clubs"]) return nb_rows def delete(self) -> tuple[int, dict[str, int]]: @@ -310,21 +309,24 @@ class MembershipQuerySet(models.QuerySet): before the deletion, and a removal of the user from the club groups. - Be aware that this adds a db query to retrieve the deleted element - and another to remove users from the groups. - As queries take place before the deletion operation, - they will be performed even if the deletion fails. + Be aware that this adds some db queries : + + - 1 to retrieve the deleted elements in order to perform + post-delete operations. + As we can't know if a delete will affect rows or not, + this query will always happen + - 1 query to remove the users from the club groups. + If the delete operation affected no row, + this query won't happen. """ - ids = list(self.values_list("club_id", "user_id")) + memberships = set(self.all()) nb_rows, rows_counts = super().delete() if nb_rows > 0: - user_ids = [i[0] for i in ids] - club_ids = [i[1] for i in ids] - self._remove_from_club_groups(user_ids, club_ids) + Membership._remove_club_groups(memberships) cache.set_many( { - f"membership_{club_id}_{user_id}": "not_member" - for club_id, user_id in ids + f"membership_{m.club_id}_{m.user_id}": "not_member" + for m in memberships } ) return nb_rows, rows_counts @@ -370,6 +372,13 @@ class Membership(models.Model): objects = MembershipQuerySet.as_manager() + class Meta: + constraints = [ + models.CheckConstraint( + check=Q(end_date__gte=F("start_date")), name="end_after_start" + ), + ] + def __str__(self): return ( f"{self.club.name} - {self.user.username} " @@ -378,24 +387,15 @@ class Membership(models.Model): ) def save(self, *args, **kwargs): - # adding must be set before calling super().save() - # because the call will switch _state.adding from True to False - adding = self._state.adding super().save(*args, **kwargs) - if adding: - groups = [ - User.groups.through( - user_id=self.user_id, group_id=self.club.members_group_id - ) - ] - if self.role > settings.SITH_MAXIMUM_FREE_ROLE: - groups.append( - User.groups.through( - user_id=self.user_id, group_id=self.club.board_group_id - ) - ) - User.groups.through.objects.bulk_create(groups) + # a save may either be an update or a creation + # and may result in either an ongoing or an ended membership. + # It could also be a retrogradation from the board to being a simple member. + # To avoid problems, the user is removed from the club groups beforehand ; + # he will be added back if necessary + self._remove_club_groups([self]) if self.end_date is None: + self._add_club_groups([self]) cache.set(f"membership_{self.club_id}_{self.user_id}", self) else: cache.set(f"membership_{self.club_id}_{self.user_id}", "not_member") @@ -403,15 +403,11 @@ class Membership(models.Model): def get_absolute_url(self): return reverse("club:club_members", kwargs={"club_id": self.club_id}) - @property - def is_ongoing(self): - return self.end_date is None or self.end_date - - def is_owned_by(self, user): + def is_owned_by(self, user: User) -> bool: """Method to see if that object can be super edited by the given user.""" if user.is_anonymous: return False - return user.is_board_member + return user.is_root or user.is_board_member def can_be_edited_by(self, user: User) -> bool: """Check if that object can be edited by the given user.""" @@ -421,9 +417,91 @@ class Membership(models.Model): return membership is not None and membership.role >= self.role def delete(self, *args, **kwargs): + self._remove_club_groups([self]) super().delete(*args, **kwargs) cache.delete(f"membership_{self.club_id}_{self.user_id}") + @staticmethod + def _remove_club_groups( + memberships: Iterable[Membership], + ) -> tuple[int, dict[str, int]]: + """Remove users of those memberships from the club groups. + + For example, if a user is in the Troll club board, + he is in the board group and the members group of the Troll. + After calling this function, he will be in neither. + + Returns: + The result of the deletion queryset. + + Warnings: + If this function isn't used in combination + with an actual deletion of the memberships, + it will result in an inconsistent state, + where users will be in the clubs, without + having the associated rights. + """ + clubs = {m.club_id for m in memberships} + users = {m.user_id for m in memberships} + groups = Group.objects.filter(Q(club__in=clubs) | Q(club_board__in=clubs)) + return User.groups.through.objects.filter( + Q(group__in=groups) & Q(user__in=users) + ).delete() + + @staticmethod + def _add_club_groups( + memberships: Iterable[Membership], + ) -> list[User.groups.through]: + """Add users of those memberships to the club groups. + + For example, if a user just joined the Troll club board, + he will be added in both the members group and the board group + of the club. + + Returns: + The created User-Group relations. + + Warnings: + If this function isn't used in combination + with an actual update/creation of the memberships, + it will result in an inconsistent state, + where users will have the rights associated to the + club, without actually being part of it. + """ + # only active membership (i.e. `end_date=None`) + # grant the attribution of club groups. + memberships = [m for m in memberships if m.end_date is None] + if not memberships: + return [] + + if sum(1 for m in memberships if not hasattr(m, "club")) > 1: + # if more than one membership hasn't its `club` attribute set + # it's less expensive to reload the whole query with + # a select_related than perform a distinct query + # to fetch each club. + ids = {m.id for m in memberships} + memberships = list( + Membership.objects.filter(id__in=ids).select_related("club") + ) + club_groups = [] + for membership in memberships: + club_groups.append( + User.groups.through( + user_id=membership.user_id, + group_id=membership.club.members_group_id, + ) + ) + if membership.role > settings.SITH_MAXIMUM_FREE_ROLE: + club_groups.append( + User.groups.through( + user_id=membership.user_id, + group_id=membership.club.board_group_id, + ) + ) + return User.groups.through.objects.bulk_create( + club_groups, ignore_conflicts=True + ) + class Mailing(models.Model): """A Mailing list for a club. diff --git a/club/tests.py b/club/tests.py index 54ea1c67..5844eeff 100644 --- a/club/tests.py +++ b/club/tests.py @@ -165,6 +165,27 @@ class TestMembershipQuerySet(TestClub): assert new_mem != "not_member" assert new_mem.role == 5 + def test_update_change_club_groups(self): + """Test that `update` set the user groups accordingly.""" + user = baker.make(User) + membership = baker.make(Membership, end_date=None, user=user, role=5) + members_group = membership.club.members_group + board_group = membership.club.board_group + assert user.groups.contains(members_group) + assert user.groups.contains(board_group) + + user.memberships.update(role=1) # from board to simple member + assert user.groups.contains(members_group) + assert not user.groups.contains(board_group) + + user.memberships.update(role=5) # from member to board + assert user.groups.contains(members_group) + assert user.groups.contains(board_group) + + user.memberships.update(end_date=localdate()) # end the membership + assert not user.groups.contains(members_group) + assert not user.groups.contains(board_group) + def test_delete_invalidate_cache(self): """Test that the `delete` queryset properly invalidate cache.""" mem_skia = self.skia.memberships.get(club=self.club) @@ -183,6 +204,19 @@ class TestMembershipQuerySet(TestClub): ) assert cached_mem == "not_member" + def test_delete_remove_from_groups(self): + """Test that `delete` removes from club groups""" + user = baker.make(User) + memberships = baker.make(Membership, role=iter([1, 5]), user=user, _quantity=2) + club_groups = { + memberships[0].club.members_group, + memberships[1].club.members_group, + memberships[1].club.board_group, + } + assert set(user.groups.all()) == club_groups + user.memberships.all().delete() + assert user.groups.all().count() == 0 + class TestClubModel(TestClub): def assert_membership_started_today(self, user: User, role: int): @@ -487,10 +521,22 @@ class TestClubModel(TestClub): """Test that when a membership begins, the user is added to the club group.""" assert not self.subscriber.groups.contains(self.club.members_group) assert not self.subscriber.groups.contains(self.club.board_group) - Membership.objects.create(club=self.club, user=self.subscriber, role=3) + baker.make(Membership, club=self.club, user=self.subscriber, role=3) assert self.subscriber.groups.contains(self.club.members_group) assert self.subscriber.groups.contains(self.club.board_group) + def test_change_position_in_club(self): + """Test that when moving from board to members, club group change""" + membership = baker.make( + Membership, club=self.club, user=self.subscriber, role=3 + ) + assert self.subscriber.groups.contains(self.club.members_group) + assert self.subscriber.groups.contains(self.club.board_group) + membership.role = 1 + membership.save() + assert self.subscriber.groups.contains(self.club.members_group) + assert not self.subscriber.groups.contains(self.club.board_group) + def test_club_owner(self): """Test that a club is owned only by board members of the main club.""" anonymous = AnonymousUser() diff --git a/core/management/commands/populate_more.py b/core/management/commands/populate_more.py index f8ac1cef..fbef3ec2 100644 --- a/core/management/commands/populate_more.py +++ b/core/management/commands/populate_more.py @@ -173,7 +173,8 @@ class Command(BaseCommand): club=club, ) ) - Membership.objects.bulk_create(memberships) + memberships = Membership.objects.bulk_create(memberships) + Membership._add_club_groups(memberships) def create_uvs(self): root = User.objects.get(username="root") From b8f851b009d0e07213a776dcc1dd4f9b6753e197 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 28 Dec 2024 12:52:17 +0100 Subject: [PATCH 08/40] translations --- locale/fr/LC_MESSAGES/django.po | 455 +++++++++++++++----------------- 1 file changed, 220 insertions(+), 235 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index d6c2e813..4b44dcf2 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-23 02:37+0100\n" +"POT-Creation-Date: 2024-12-28 12:50+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -40,7 +40,7 @@ msgstr "code postal" msgid "country" msgstr "pays" -#: accounting/models.py:67 core/models.py:356 +#: accounting/models.py:67 core/models.py:279 msgid "phone" msgstr "téléphone" @@ -64,7 +64,7 @@ msgstr "IBAN" msgid "account number" msgstr "numéro de compte" -#: accounting/models.py:107 accounting/models.py:136 club/models.py:345 +#: accounting/models.py:107 accounting/models.py:136 club/models.py:356 #: com/models.py:87 com/models.py:272 com/models.py:312 counter/models.py:361 #: counter/models.py:485 trombi/models.py:209 msgid "club" @@ -87,12 +87,12 @@ msgstr "Compte club" msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:188 club/models.py:351 counter/models.py:997 +#: accounting/models.py:188 club/models.py:362 counter/models.py:1000 #: election/models.py:16 launderette/models.py:165 msgid "start date" msgstr "date de début" -#: accounting/models.py:189 club/models.py:352 counter/models.py:998 +#: accounting/models.py:189 club/models.py:363 counter/models.py:1001 #: election/models.py:17 msgid "end date" msgstr "date de fin" @@ -106,7 +106,7 @@ msgid "club account" msgstr "compte club" #: accounting/models.py:199 accounting/models.py:255 counter/models.py:93 -#: counter/models.py:714 +#: counter/models.py:717 msgid "amount" msgstr "montant" @@ -126,20 +126,20 @@ msgstr "numéro" msgid "journal" msgstr "classeur" -#: accounting/models.py:256 core/models.py:905 core/models.py:1414 -#: core/models.py:1459 core/models.py:1488 core/models.py:1512 -#: counter/models.py:724 counter/models.py:829 counter/models.py:1033 +#: accounting/models.py:256 core/models.py:815 core/models.py:1324 +#: core/models.py:1369 core/models.py:1398 core/models.py:1422 +#: counter/models.py:727 counter/models.py:832 counter/models.py:1036 #: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 #: forum/models.py:413 msgid "date" msgstr "date" -#: accounting/models.py:257 counter/models.py:302 counter/models.py:1034 +#: accounting/models.py:257 counter/models.py:302 counter/models.py:1037 #: pedagogy/models.py:208 msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:726 counter/models.py:831 +#: accounting/models.py:259 counter/models.py:729 counter/models.py:834 #: subscription/models.py:56 msgid "payment method" msgstr "méthode de paiement" @@ -165,8 +165,8 @@ msgid "accounting type" msgstr "type comptable" #: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 -#: accounting/models.py:492 core/models.py:1487 core/models.py:1513 -#: counter/models.py:795 +#: accounting/models.py:492 core/models.py:1397 core/models.py:1423 +#: counter/models.py:798 msgid "label" msgstr "étiquette" @@ -174,7 +174,7 @@ msgstr "étiquette" msgid "target type" msgstr "type de cible" -#: accounting/models.py:303 club/models.py:504 +#: accounting/models.py:303 club/models.py:611 #: club/templates/club/club_members.jinja:17 #: club/templates/club/club_old_members.jinja:8 #: club/templates/club/mailing.jinja:41 @@ -186,7 +186,7 @@ msgstr "type de cible" msgid "User" msgstr "Utilisateur" -#: accounting/models.py:304 club/models.py:408 +#: accounting/models.py:304 club/models.py:515 #: club/templates/club/club_detail.jinja:12 #: com/templates/com/mailing_admin.jinja:11 #: com/templates/com/news_admin_list.jinja:23 @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:307 core/models.py:303 sith/settings.py:424 +#: accounting/models.py:307 core/models.py:226 sith/settings.py:424 msgid "Other" msgstr "Autre" @@ -385,7 +385,7 @@ msgid "Delete" msgstr "Supprimer" #: accounting/templates/accounting/bank_account_details.jinja:18 -#: club/views.py:79 core/views/user.py:201 sas/templates/sas/picture.jinja:91 +#: club/views.py:78 core/views/user.py:201 sas/templates/sas/picture.jinja:91 msgid "Infos" msgstr "Infos" @@ -404,7 +404,7 @@ msgstr "Nouveau compte club" #: accounting/templates/accounting/bank_account_details.jinja:27 #: accounting/templates/accounting/bank_account_list.jinja:22 #: accounting/templates/accounting/club_account_details.jinja:58 -#: accounting/templates/accounting/journal_details.jinja:92 club/views.py:125 +#: accounting/templates/accounting/journal_details.jinja:92 club/views.py:124 #: com/templates/com/news_admin_list.jinja:39 #: com/templates/com/news_admin_list.jinja:68 #: com/templates/com/news_admin_list.jinja:115 @@ -586,7 +586,7 @@ msgstr "Classeur : " #: accounting/templates/accounting/journal_statement_accounting.jinja:30 #: core/templates/core/user_account.jinja:39 #: core/templates/core/user_account_detail.jinja:9 -#: counter/templates/counter/counter_click.jinja:46 +#: counter/templates/counter/counter_click.jinja:39 msgid "Amount: " msgstr "Montant : " @@ -934,11 +934,11 @@ msgstr "Retirer" msgid "Action" msgstr "Action" -#: club/forms.py:109 club/tests.py:711 +#: club/forms.py:109 club/tests.py:742 msgid "This field is required" msgstr "Ce champ est obligatoire" -#: club/forms.py:118 club/tests.py:742 +#: club/forms.py:118 club/tests.py:773 msgid "One of the selected users doesn't have an email address" msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email" @@ -946,15 +946,15 @@ msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email" msgid "An action is required" msgstr "Une action est requise" -#: club/forms.py:140 club/tests.py:698 club/tests.py:724 +#: club/forms.py:140 club/tests.py:729 club/tests.py:755 msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:207 +#: club/forms.py:149 counter/forms.py:209 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:210 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:212 #: election/views.py:170 subscription/forms.py:21 msgid "End date" msgstr "Date de fin" @@ -1025,74 +1025,70 @@ msgstr "actif" msgid "short description" msgstr "description courte" -#: club/models.py:81 core/models.py:358 +#: club/models.py:81 core/models.py:281 msgid "address" msgstr "Adresse" -#: club/models.py:98 core/models.py:269 +#: club/models.py:98 core/models.py:192 msgid "home" msgstr "home" -#: club/models.py:150 +#: club/models.py:159 msgid "You can not make loops in clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs" -#: club/models.py:174 -msgid "A club with that unix_name already exists" -msgstr "Un club avec ce nom UNIX existe déjà." - -#: club/models.py:337 counter/models.py:988 counter/models.py:1024 +#: club/models.py:348 counter/models.py:991 counter/models.py:1027 #: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 #: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 #: trombi/models.py:205 msgid "user" msgstr "utilisateur" -#: club/models.py:354 core/models.py:322 election/models.py:178 +#: club/models.py:365 core/models.py:245 election/models.py:178 #: election/models.py:212 trombi/models.py:210 msgid "role" msgstr "rôle" -#: club/models.py:359 core/models.py:84 counter/models.py:300 +#: club/models.py:370 core/models.py:69 counter/models.py:300 #: counter/models.py:333 election/models.py:13 election/models.py:115 #: election/models.py:188 forum/models.py:61 forum/models.py:245 msgid "description" msgstr "description" -#: club/models.py:415 club/models.py:510 +#: club/models.py:522 club/models.py:617 msgid "Email address" msgstr "Adresse email" -#: club/models.py:423 +#: club/models.py:530 msgid "Enter a valid address. Only the root of the address is needed." msgstr "" "Entrez une adresse valide. Seule la racine de l'adresse est nécessaire." -#: club/models.py:427 com/models.py:97 com/models.py:322 core/models.py:906 +#: club/models.py:534 com/models.py:97 com/models.py:322 core/models.py:816 msgid "is moderated" msgstr "est modéré" -#: club/models.py:431 com/models.py:101 com/models.py:326 +#: club/models.py:538 com/models.py:101 com/models.py:326 msgid "moderator" msgstr "modérateur" -#: club/models.py:457 +#: club/models.py:564 msgid "This mailing list already exists." msgstr "Cette liste de diffusion existe déjà." -#: club/models.py:496 club/templates/club/mailing.jinja:23 +#: club/models.py:603 club/templates/club/mailing.jinja:23 msgid "Mailing" msgstr "Liste de diffusion" -#: club/models.py:520 +#: club/models.py:627 msgid "At least user or email is required" msgstr "Au moins un utilisateur ou un email est nécessaire" -#: club/models.py:528 club/tests.py:770 +#: club/models.py:635 club/tests.py:801 msgid "This email is already suscribed in this mailing" msgstr "Cet email est déjà abonné à cette mailing" -#: club/models.py:556 +#: club/models.py:663 msgid "Unregistered user" msgstr "Utilisateur non enregistré" @@ -1146,7 +1142,7 @@ msgid "There are no members in this club." msgstr "Il n'y a pas de membres dans ce club." #: club/templates/club/club_members.jinja:80 -#: core/templates/core/file_detail.jinja:19 core/views/forms.py:309 +#: core/templates/core/file_detail.jinja:19 core/views/forms.py:308 #: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19 msgid "Add" msgstr "Ajouter" @@ -1217,7 +1213,7 @@ msgid "Barman" msgstr "Barman" #: club/templates/club/club_sellings.jinja:51 -#: counter/templates/counter/counter_click.jinja:43 +#: counter/templates/counter/counter_click.jinja:36 #: counter/templates/counter/last_ops.jinja:22 #: counter/templates/counter/last_ops.jinja:47 #: counter/templates/counter/refilling_list.jinja:12 @@ -1359,41 +1355,41 @@ msgstr "Aucune page n'existe pour ce club" msgid "Club stats" msgstr "Statistiques du club" -#: club/views.py:89 +#: club/views.py:88 msgid "Members" msgstr "Membres" -#: club/views.py:98 +#: club/views.py:97 msgid "Old members" msgstr "Anciens membres" -#: club/views.py:108 core/templates/core/page.jinja:33 +#: club/views.py:107 core/templates/core/page.jinja:33 msgid "History" msgstr "Historique" -#: club/views.py:116 core/templates/core/base/header.jinja:61 +#: club/views.py:115 core/templates/core/base/header.jinja:61 #: core/views/user.py:224 sas/templates/sas/picture.jinja:110 #: trombi/views.py:62 msgid "Tools" msgstr "Outils" -#: club/views.py:136 +#: club/views.py:135 msgid "Edit club page" msgstr "Éditer la page de club" -#: club/views.py:145 club/views.py:452 +#: club/views.py:144 club/views.py:451 msgid "Sellings" msgstr "Vente" -#: club/views.py:152 +#: club/views.py:151 msgid "Mailing list" msgstr "Listes de diffusion" -#: club/views.py:161 com/views.py:134 +#: club/views.py:160 com/views.py:134 msgid "Posters list" msgstr "Liste d'affiches" -#: club/views.py:171 counter/templates/counter/counter_list.jinja:21 +#: club/views.py:170 counter/templates/counter/counter_list.jinja:21 #: counter/templates/counter/counter_list.jinja:37 #: counter/templates/counter/counter_list.jinja:53 msgid "Props" @@ -1454,7 +1450,7 @@ msgstr "contenu" msgid "A more detailed and exhaustive description of the event." msgstr "Une description plus détaillée et exhaustive de l'évènement." -#: com/models.py:82 core/models.py:1457 launderette/models.py:88 +#: com/models.py:82 core/models.py:1367 launderette/models.py:88 #: launderette/models.py:124 launderette/models.py:167 msgid "type" msgstr "type" @@ -1508,7 +1504,7 @@ msgstr "weekmail" msgid "rank" msgstr "rang" -#: com/models.py:308 core/models.py:871 core/models.py:921 +#: com/models.py:308 core/models.py:781 core/models.py:831 msgid "file" msgstr "fichier" @@ -1992,48 +1988,49 @@ msgstr "" "Vous devez êtres un membre du bureau du club sélectionné pour poster dans le " "Weekmail." -#: core/models.py:79 -msgid "meta group status" -msgstr "status du meta-groupe" +#: core/models.py:64 +msgid "Is manually manageable" +msgstr "Est gérable manuellement" -#: core/models.py:81 -msgid "Whether a group is a meta group or not" -msgstr "Si un groupe est un meta-groupe ou pas" +#: core/models.py:66 +msgid "If False, this shouldn't be shown on group management pages" +msgstr "" +"Si faux, ceci ne devrait pas être montré sur les pages de gestion des groupes" -#: core/models.py:167 +#: core/models.py:90 #, python-format msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" -#: core/models.py:250 +#: core/models.py:173 msgid "first name" msgstr "Prénom" -#: core/models.py:251 +#: core/models.py:174 msgid "last name" msgstr "Nom" -#: core/models.py:252 +#: core/models.py:175 msgid "email address" msgstr "adresse email" -#: core/models.py:253 +#: core/models.py:176 msgid "date of birth" msgstr "date de naissance" -#: core/models.py:254 +#: core/models.py:177 msgid "nick name" msgstr "surnom" -#: core/models.py:255 +#: core/models.py:178 msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:258 +#: core/models.py:181 msgid "groups" msgstr "groupes" -#: core/models.py:260 +#: core/models.py:183 msgid "" "The groups this user belongs to. A user will get all permissions granted to " "each of their groups." @@ -2041,147 +2038,147 @@ msgstr "" "Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes " "les permissions de chacun de ses groupes." -#: core/models.py:277 +#: core/models.py:200 msgid "profile" msgstr "profil" -#: core/models.py:285 +#: core/models.py:208 msgid "avatar" msgstr "avatar" -#: core/models.py:293 +#: core/models.py:216 msgid "scrub" msgstr "blouse" -#: core/models.py:299 +#: core/models.py:222 msgid "sex" msgstr "Genre" -#: core/models.py:303 +#: core/models.py:226 msgid "Man" msgstr "Homme" -#: core/models.py:303 +#: core/models.py:226 msgid "Woman" msgstr "Femme" -#: core/models.py:305 +#: core/models.py:228 msgid "pronouns" msgstr "pronoms" -#: core/models.py:307 +#: core/models.py:230 msgid "tshirt size" msgstr "taille de t-shirt" -#: core/models.py:310 +#: core/models.py:233 msgid "-" msgstr "-" -#: core/models.py:311 +#: core/models.py:234 msgid "XS" msgstr "XS" -#: core/models.py:312 +#: core/models.py:235 msgid "S" msgstr "S" -#: core/models.py:313 +#: core/models.py:236 msgid "M" msgstr "M" -#: core/models.py:314 +#: core/models.py:237 msgid "L" msgstr "L" -#: core/models.py:315 +#: core/models.py:238 msgid "XL" msgstr "XL" -#: core/models.py:316 +#: core/models.py:239 msgid "XXL" msgstr "XXL" -#: core/models.py:317 +#: core/models.py:240 msgid "XXXL" msgstr "XXXL" -#: core/models.py:325 +#: core/models.py:248 msgid "Student" msgstr "Étudiant" -#: core/models.py:326 +#: core/models.py:249 msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:327 +#: core/models.py:250 msgid "Teacher" msgstr "Enseignant" -#: core/models.py:328 +#: core/models.py:251 msgid "Agent" msgstr "Personnel" -#: core/models.py:329 +#: core/models.py:252 msgid "Doctor" msgstr "Doctorant" -#: core/models.py:330 +#: core/models.py:253 msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:331 +#: core/models.py:254 msgid "Service" msgstr "Service" -#: core/models.py:337 +#: core/models.py:260 msgid "department" msgstr "département" -#: core/models.py:344 +#: core/models.py:267 msgid "dpt option" msgstr "Filière" -#: core/models.py:346 pedagogy/models.py:70 pedagogy/models.py:294 +#: core/models.py:269 pedagogy/models.py:70 pedagogy/models.py:294 msgid "semester" msgstr "semestre" -#: core/models.py:347 +#: core/models.py:270 msgid "quote" msgstr "citation" -#: core/models.py:348 +#: core/models.py:271 msgid "school" msgstr "école" -#: core/models.py:350 +#: core/models.py:273 msgid "promo" msgstr "promo" -#: core/models.py:353 +#: core/models.py:276 msgid "forum signature" msgstr "signature du forum" -#: core/models.py:355 +#: core/models.py:278 msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:357 +#: core/models.py:280 msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:360 +#: core/models.py:283 msgid "parent address" msgstr "adresse des parents" -#: core/models.py:363 +#: core/models.py:286 msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:556 +#: core/models.py:466 msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:710 core/templates/core/macros.jinja:80 +#: core/models.py:620 core/templates/core/macros.jinja:80 #: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85 #: core/templates/core/user_detail.jinja:100 #: core/templates/core/user_detail.jinja:101 @@ -2201,101 +2198,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgid "Profile" msgstr "Profil" -#: core/models.py:821 +#: core/models.py:731 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:828 +#: core/models.py:738 msgid "receive the Weekmail" msgstr "recevoir le Weekmail" -#: core/models.py:829 +#: core/models.py:739 msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:831 +#: core/models.py:741 msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:834 +#: core/models.py:744 msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:860 sas/forms.py:81 +#: core/models.py:770 sas/forms.py:81 msgid "file name" msgstr "nom du fichier" -#: core/models.py:864 core/models.py:1215 +#: core/models.py:774 core/models.py:1125 msgid "parent" msgstr "parent" -#: core/models.py:878 +#: core/models.py:788 msgid "compressed file" msgstr "version allégée" -#: core/models.py:885 +#: core/models.py:795 msgid "thumbnail" msgstr "miniature" -#: core/models.py:893 core/models.py:910 +#: core/models.py:803 core/models.py:820 msgid "owner" msgstr "propriétaire" -#: core/models.py:897 core/models.py:1232 +#: core/models.py:807 core/models.py:1142 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:900 core/models.py:1235 +#: core/models.py:810 core/models.py:1145 msgid "view group" msgstr "groupe de vue" -#: core/models.py:902 +#: core/models.py:812 msgid "is folder" msgstr "est un dossier" -#: core/models.py:903 +#: core/models.py:813 msgid "mime type" msgstr "type mime" -#: core/models.py:904 +#: core/models.py:814 msgid "size" msgstr "taille" -#: core/models.py:915 +#: core/models.py:825 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:917 +#: core/models.py:827 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:984 +#: core/models.py:894 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:986 core/models.py:990 +#: core/models.py:896 core/models.py:900 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:993 +#: core/models.py:903 msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:1004 +#: core/models.py:914 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:1021 +#: core/models.py:931 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1198 +#: core/models.py:1108 msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1204 +#: core/models.py:1114 msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2303,55 +2300,55 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1222 +#: core/models.py:1132 msgid "page name" msgstr "nom de la page" -#: core/models.py:1227 +#: core/models.py:1137 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1240 +#: core/models.py:1150 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1247 +#: core/models.py:1157 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1297 +#: core/models.py:1207 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1300 +#: core/models.py:1210 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1411 +#: core/models.py:1321 msgid "revision" msgstr "révision" -#: core/models.py:1412 +#: core/models.py:1322 msgid "page title" msgstr "titre de la page" -#: core/models.py:1413 +#: core/models.py:1323 msgid "page content" msgstr "contenu de la page" -#: core/models.py:1454 +#: core/models.py:1364 msgid "url" msgstr "url" -#: core/models.py:1455 +#: core/models.py:1365 msgid "param" msgstr "param" -#: core/models.py:1460 +#: core/models.py:1370 msgid "viewed" msgstr "vue" -#: core/models.py:1518 +#: core/models.py:1428 msgid "operation type" msgstr "type d'opération" @@ -2562,7 +2559,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:125 +#: counter/templates/counter/counter_click.jinja:155 #: counter/templates/counter/fragments/delete_student_card.jinja:12 #: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" @@ -2676,7 +2673,7 @@ msgid "Edit group" msgstr "Éditer le groupe" #: core/templates/core/group_edit.jinja:9 -#: core/templates/core/user_edit.jinja:170 +#: core/templates/core/user_edit.jinja:167 #: core/templates/core/user_group.jinja:13 #: pedagogy/templates/pedagogy/uv_edit.jinja:36 msgid "Update" @@ -3162,27 +3159,27 @@ msgstr "Activer la caméra" msgid "Take a picture" msgstr "Prendre une photo" -#: core/templates/core/user_edit.jinja:69 +#: core/templates/core/user_edit.jinja:67 msgid "To edit your profile picture, ask a member of the AE" msgstr "Pour changer votre photo de profil, demandez à un membre de l'AE" -#: core/templates/core/user_edit.jinja:98 +#: core/templates/core/user_edit.jinja:96 msgid "Edit user profile" msgstr "Éditer le profil de l'utilisateur" -#: core/templates/core/user_edit.jinja:160 +#: core/templates/core/user_edit.jinja:157 msgid "Change my password" msgstr "Changer mon mot de passe" -#: core/templates/core/user_edit.jinja:165 +#: core/templates/core/user_edit.jinja:162 msgid "Change user password" msgstr "Changer le mot de passe" -#: core/templates/core/user_edit.jinja:175 +#: core/templates/core/user_edit.jinja:173 msgid "Username:" msgstr "Nom d'utilisateur : " -#: core/templates/core/user_edit.jinja:178 +#: core/templates/core/user_edit.jinja:176 msgid "Account number:" msgstr "Numéro de compte : " @@ -3276,7 +3273,7 @@ msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" #: core/templates/core/user_preferences.jinja:39 -#: counter/templates/counter/counter_click.jinja:149 +#: counter/templates/counter/counter_click.jinja:180 msgid "Student card" msgstr "Carte étudiante" @@ -3326,7 +3323,7 @@ msgstr "Outils utilisateurs" msgid "Sith management" msgstr "Gestion de Sith" -#: core/templates/core/user_tools.jinja:21 core/views/forms.py:295 +#: core/templates/core/user_tools.jinja:21 core/views/forms.py:293 #: core/views/user.py:254 msgid "Groups" msgstr "Groupes" @@ -3355,7 +3352,7 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:180 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:182 #: counter/views/mixins.py:89 msgid "Counters" msgstr "Comptoirs" @@ -3477,7 +3474,7 @@ msgstr "Ajouter un nouveau dossier" msgid "Error creating folder %(folder_name)s: %(msg)s" msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s" -#: core/views/files.py:159 core/views/forms.py:270 core/views/forms.py:277 +#: core/views/files.py:159 core/views/forms.py:268 core/views/forms.py:275 #: sas/forms.py:60 #, python-format msgid "Error uploading file %(file_name)s: %(msg)s" @@ -3487,19 +3484,19 @@ msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s" msgid "Apply rights recursively" msgstr "Appliquer les droits récursivement" -#: core/views/forms.py:95 core/views/forms.py:103 +#: core/views/forms.py:90 core/views/forms.py:98 msgid "Choose file" msgstr "Choisir un fichier" -#: core/views/forms.py:119 core/views/forms.py:127 +#: core/views/forms.py:114 core/views/forms.py:122 msgid "Choose user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:159 +#: core/views/forms.py:154 msgid "Username, email, or account number" msgstr "Nom d'utilisateur, email, ou numéro de compte AE" -#: core/views/forms.py:220 +#: core/views/forms.py:218 msgid "" "Profile: you need to be visible on the picture, in order to be recognized (e." "g. by the barmen)" @@ -3507,44 +3504,44 @@ msgstr "" "Photo de profil: vous devez être visible sur la photo afin d'être reconnu " "(par exemple par les barmen)" -#: core/views/forms.py:225 +#: core/views/forms.py:223 msgid "Avatar: used on the forum" msgstr "Avatar : utilisé sur le forum" -#: core/views/forms.py:229 +#: core/views/forms.py:227 msgid "Scrub: let other know how your scrub looks like!" msgstr "Blouse : montrez aux autres à quoi ressemble votre blouse !" -#: core/views/forms.py:281 +#: core/views/forms.py:279 msgid "Bad image format, only jpeg, png, webp and gif are accepted" msgstr "Mauvais format d'image, seuls les jpeg, png, webp et gif sont acceptés" -#: core/views/forms.py:306 +#: core/views/forms.py:305 msgid "Godfather / Godmother" msgstr "Parrain / Marraine" -#: core/views/forms.py:307 +#: core/views/forms.py:306 msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:312 counter/forms.py:78 trombi/views.py:151 +#: core/views/forms.py:311 counter/forms.py:80 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:326 +#: core/views/forms.py:325 msgid "This user does not exist" msgstr "Cet utilisateur n'existe pas" -#: core/views/forms.py:328 +#: core/views/forms.py:327 msgid "You cannot be related to yourself" msgstr "Vous ne pouvez pas être relié à vous-même" -#: core/views/forms.py:340 +#: core/views/forms.py:339 #, python-format msgid "%s is already your godfather" msgstr "%s est déjà votre parrain/marraine" -#: core/views/forms.py:346 +#: core/views/forms.py:345 #, python-format msgid "%s is already your godchild" msgstr "%s est déjà votre fillot/fillote" @@ -3583,21 +3580,21 @@ msgstr "Chèque" msgid "Cash" msgstr "Espèces" -#: counter/apps.py:30 counter/models.py:833 sith/settings.py:416 +#: counter/apps.py:30 counter/models.py:836 sith/settings.py:416 #: sith/settings.py:421 msgid "Credit card" msgstr "Carte bancaire" -#: counter/apps.py:36 counter/models.py:509 counter/models.py:994 -#: counter/models.py:1030 launderette/models.py:32 +#: counter/apps.py:36 counter/models.py:509 counter/models.py:997 +#: counter/models.py:1033 launderette/models.py:32 msgid "counter" msgstr "comptoir" -#: counter/forms.py:59 +#: counter/forms.py:61 msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:108 +#: counter/forms.py:110 msgid "User not found" msgstr "Utilisateur non trouvé" @@ -3721,7 +3718,7 @@ msgstr "groupe d'achat" msgid "archived" msgstr "archivé" -#: counter/models.py:371 counter/models.py:1128 +#: counter/models.py:371 counter/models.py:1131 msgid "product" msgstr "produit" @@ -3749,44 +3746,44 @@ msgstr "vendeurs" msgid "token" msgstr "jeton" -#: counter/models.py:732 +#: counter/models.py:735 msgid "bank" msgstr "banque" -#: counter/models.py:734 counter/models.py:836 +#: counter/models.py:737 counter/models.py:839 msgid "is validated" msgstr "est validé" -#: counter/models.py:739 +#: counter/models.py:742 msgid "refilling" msgstr "rechargement" -#: counter/models.py:813 eboutic/models.py:249 +#: counter/models.py:816 eboutic/models.py:249 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:814 counter/models.py:1108 eboutic/models.py:250 +#: counter/models.py:817 counter/models.py:1111 eboutic/models.py:250 msgid "quantity" msgstr "quantité" -#: counter/models.py:833 +#: counter/models.py:836 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:841 +#: counter/models.py:844 msgid "selling" msgstr "vente" -#: counter/models.py:945 +#: counter/models.py:948 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:946 +#: counter/models.py:949 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:948 counter/models.py:961 +#: counter/models.py:951 counter/models.py:964 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3798,67 +3795,67 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:999 +#: counter/models.py:1002 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:1002 +#: counter/models.py:1005 msgid "permanency" msgstr "permanence" -#: counter/models.py:1035 +#: counter/models.py:1038 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1038 +#: counter/models.py:1041 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1104 +#: counter/models.py:1107 msgid "cash summary" msgstr "relevé" -#: counter/models.py:1107 +#: counter/models.py:1110 msgid "value" msgstr "valeur" -#: counter/models.py:1110 +#: counter/models.py:1113 msgid "check" msgstr "chèque" -#: counter/models.py:1112 +#: counter/models.py:1115 msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1116 +#: counter/models.py:1119 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1132 +#: counter/models.py:1135 msgid "banner" msgstr "bannière" -#: counter/models.py:1134 +#: counter/models.py:1137 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1136 +#: counter/models.py:1139 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1138 +#: counter/models.py:1141 msgid "secret" msgstr "secret" -#: counter/models.py:1177 +#: counter/models.py:1180 msgid "uid" msgstr "uid" -#: counter/models.py:1182 counter/models.py:1187 +#: counter/models.py:1185 counter/models.py:1190 msgid "student card" msgstr "carte étudiante" -#: counter/models.py:1188 +#: counter/models.py:1191 msgid "student cards" msgstr "cartes étudiantes" @@ -3918,28 +3915,28 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:55 +#: counter/templates/counter/counter_click.jinja:48 #: launderette/templates/launderette/launderette_admin.jinja:8 msgid "Selling" msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:66 +#: counter/templates/counter/counter_click.jinja:55 msgid "Select a product..." msgstr "Sélectionnez un produit…" -#: counter/templates/counter/counter_click.jinja:68 +#: counter/templates/counter/counter_click.jinja:57 msgid "Operations" msgstr "Opérations" -#: counter/templates/counter/counter_click.jinja:69 +#: counter/templates/counter/counter_click.jinja:58 msgid "Confirm (FIN)" msgstr "Confirmer (FIN)" -#: counter/templates/counter/counter_click.jinja:70 +#: counter/templates/counter/counter_click.jinja:59 msgid "Cancel (ANN)" msgstr "Annuler (ANN)" -#: counter/templates/counter/counter_click.jinja:81 +#: counter/templates/counter/counter_click.jinja:70 #: counter/templates/counter/fragments/create_refill.jinja:8 #: counter/templates/counter/fragments/create_student_card.jinja:10 #: counter/templates/counter/invoices_call.jinja:16 @@ -3950,25 +3947,25 @@ msgstr "Annuler (ANN)" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:89 +#: counter/templates/counter/counter_click.jinja:78 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:95 +#: counter/templates/counter/counter_click.jinja:97 msgid "This basket is empty" msgstr "Votre panier est vide" -#: counter/templates/counter/counter_click.jinja:124 +#: counter/templates/counter/counter_click.jinja:150 msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:130 +#: counter/templates/counter/counter_click.jinja:161 #: counter/templates/counter/refilling_list.jinja:9 msgid "Refilling" msgstr "Rechargement" -#: counter/templates/counter/counter_click.jinja:140 +#: counter/templates/counter/counter_click.jinja:171 msgid "" "As a barman, you are not able to refill any account on your own. An admin " "should be connected on this counter for that. The customer can refill by " @@ -3978,7 +3975,7 @@ msgstr "" "vous même. Un admin doit être connecté sur ce comptoir pour cela. Le client " "peut recharger son compte en utilisant l'eboutic" -#: counter/templates/counter/counter_click.jinja:161 +#: counter/templates/counter/counter_click.jinja:192 msgid "No products available on this counter for this user" msgstr "Pas de produits disponnibles dans ce comptoir pour cet utilisateur" @@ -4426,7 +4423,7 @@ msgid "Clear" msgstr "Vider" #: eboutic/templates/eboutic/eboutic_main.jinja:72 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:95 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:94 msgid "Validate" msgstr "Valider" @@ -4461,7 +4458,7 @@ msgstr "Solde restant : " msgid "Billing information" msgstr "Informations de facturation" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:103 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:102 msgid "" "You must fill your billing infos if you want to pay with your credit\n" " card" @@ -4469,7 +4466,7 @@ msgstr "" "Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "par carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:108 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:107 msgid "" "\n" " The Crédit Agricole changed its policy related to the " @@ -4487,32 +4484,32 @@ msgstr "" "souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux " "données que vous aviez déjà fourni." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:124 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:123 msgid "Pay with credit card" msgstr "Payer avec une carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:129 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:128 msgid "" "AE account payment disabled because your basket contains refilling items." msgstr "" "Paiement par compte AE désactivé parce que votre panier contient des bons de " "rechargement." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:131 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:130 msgid "" "AE account payment disabled because you do not have enough money remaining." msgstr "" "Paiement par compte AE désactivé parce que votre solde est insuffisant." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:136 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:135 msgid "Pay with Sith account" msgstr "Payer avec un compte AE" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:147 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:146 msgid "Billing info registration success" msgstr "Informations de facturation enregistrées" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:148 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja:147 msgid "Billing info registration failure" msgstr "Echec de l'enregistrement des informations de facturation." @@ -6209,15 +6206,3 @@ msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." #, python-format msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" - -#~ msgid "Too young for that product" -#~ msgstr "Trop jeune pour ce produit" - -#~ msgid "Not allowed for that product" -#~ msgstr "Non autorisé pour ce produit" - -#~ msgid "No date of birth provided" -#~ msgstr "Pas de date de naissance renseignée" - -#~ msgid "You have not enough money to buy all the basket" -#~ msgstr "Vous n'avez pas assez d'argent pour acheter le panier" From efca10e252af1d4339315dbdecfc6dffc296e14f Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 3 Jan 2025 17:30:24 +0100 Subject: [PATCH 09/40] remove `Club.view_groups`, `Club.edit_groups` and `Club.owner_group` --- club/admin.py | 3 -- ...012_club_board_group_club_members_group.py | 16 ++++++++-- club/models.py | 29 ++++--------------- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/club/admin.py b/club/admin.py index 2515e208..04265245 100644 --- a/club/admin.py +++ b/club/admin.py @@ -25,9 +25,6 @@ class ClubAdmin(admin.ModelAdmin): "parent", "board_group", "members_group", - "view_groups", - "edit_groups", - "owner_group", "home", "page", ) diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py index 41520c22..2a93dd38 100644 --- a/club/migrations/0012_club_board_group_club_members_group.py +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -64,13 +64,25 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveField( + model_name="club", + name="edit_groups", + ), + migrations.RemoveField( + model_name="club", + name="owner_group", + ), + migrations.RemoveField( + model_name="club", + name="view_groups", + ), migrations.AddField( model_name="club", name="board_group", field=models.OneToOneField( blank=True, null=True, - on_delete=django.db.models.deletion.CASCADE, + on_delete=django.db.models.deletion.PROTECT, related_name="club_board", to="core.group", ), @@ -81,7 +93,7 @@ class Migration(migrations.Migration): field=models.OneToOneField( blank=True, null=True, - on_delete=django.db.models.deletion.CASCADE, + on_delete=django.db.models.deletion.PROTECT, related_name="club", to="core.group", ), diff --git a/club/models.py b/club/models.py index 2b74155f..8785b517 100644 --- a/club/models.py +++ b/club/models.py @@ -79,19 +79,6 @@ class Club(models.Model): _("short description"), max_length=1000, default="", blank=True, null=True ) address = models.CharField(_("address"), max_length=254) - - owner_group = models.ForeignKey( - Group, - related_name="owned_club", - default=get_default_owner_group, - on_delete=models.CASCADE, - ) - edit_groups = models.ManyToManyField( - Group, related_name="editable_club", blank=True - ) - view_groups = models.ManyToManyField( - Group, related_name="viewable_club", blank=True - ) home = models.OneToOneField( SithFile, related_name="home_of_club", @@ -104,10 +91,10 @@ class Club(models.Model): Page, related_name="club", blank=True, null=True, on_delete=models.CASCADE ) members_group = models.OneToOneField( - Group, related_name="club", on_delete=models.CASCADE + Group, related_name="club", on_delete=models.PROTECT ) board_group = models.OneToOneField( - Group, related_name="club_board", on_delete=models.CASCADE + Group, related_name="club_board", on_delete=models.PROTECT ) class Meta: @@ -131,12 +118,7 @@ class Club(models.Model): ) super().save(*args, **kwargs) if creation: - subscribers = Group.objects.filter( - name=settings.SITH_MAIN_MEMBERS_GROUP - ).first() self.make_home() - self.home.edit_groups.add(self.board_group) - self.home.view_groups.add(self.members_group, subscribers) self.make_page() cache.set(f"sith_club_{self.unix_name}", self) @@ -209,6 +191,8 @@ class Club(models.Model): for membership in self.members.ongoing().select_related("user"): cache.delete(f"membership_{self.id}_{membership.user.id}") cache.delete(f"sith_club_{self.unix_name}") + self.board_group.delete() + self.members_group.delete() return super().delete(*args, **kwargs) def get_display_name(self) -> str: @@ -218,7 +202,7 @@ class Club(models.Model): """Method to see if that object can be super edited by the given user.""" if user.is_anonymous: return False - return user.is_board_member + return user.is_root or user.is_board_member def get_full_logo_url(self) -> str: return f"https://{settings.SITH_URL}{self.logo.url}" @@ -251,8 +235,7 @@ class Club(models.Model): return membership def has_rights_in_club(self, user: User) -> bool: - m = self.get_membership_for(user) - return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE + return user.is_in_group(pk=self.board_group_id) class MembershipQuerySet(models.QuerySet): From bb210f8d47327551db4e17d7e4fe8a58defab166 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 4 Jan 2025 16:43:38 +0100 Subject: [PATCH 10/40] change club group names when the club name changes --- club/models.py | 13 ++++++++++--- club/tests.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/club/models.py b/club/models.py index 8785b517..4184715a 100644 --- a/club/models.py +++ b/club/models.py @@ -106,9 +106,16 @@ class Club(models.Model): @transaction.atomic() def save(self, *args, **kwargs): creation = self._state.adding - if not creation and Club.objects.get(id=self.id).unix_name != self.unix_name: - self.home.name = self.unix_name - self.home.save() + if not creation: + db_club = Club.objects.get(id=self.id) + if self.unix_name != db_club.unix_name: + self.home.name = self.unix_name + self.home.save() + if self.name != db_club.name: + self.board_group.name = f"{self.name} - Bureau" + self.board_group.save() + self.members_group.name = f"{self.name} - Membres" + self.members_group.save() if creation: self.board_group = Group.objects.create( name=f"{self.name} - Bureau", is_manually_manageable=False diff --git a/club/tests.py b/club/tests.py index 5844eeff..b81aa38d 100644 --- a/club/tests.py +++ b/club/tests.py @@ -548,6 +548,26 @@ class TestClubModel(TestClub): Membership(club=self.ae, user=self.sli, role=3).save() assert self.club.is_owned_by(self.sli) + def test_change_club_name(self): + """Test that changing the club name doesn't break things.""" + members_group = self.club.members_group + board_group = self.club.board_group + initial_members = set(members_group.users.values_list("id", flat=True)) + initial_board = set(board_group.users.values_list("id", flat=True)) + self.club.name = "something else" + self.club.save() + self.club.refresh_from_db() + + # The names should have changed, but not the ids nor the group members + assert self.club.members_group.name == "something else - Membres" + assert self.club.board_group.name == "something else - Bureau" + assert self.club.members_group.id == members_group.id + assert self.club.board_group.id == board_group.id + new_members = set(self.club.members_group.users.values_list("id", flat=True)) + new_board = set(self.club.board_group.users.values_list("id", flat=True)) + assert new_members == initial_members + assert new_board == initial_board + class TestMailingForm(TestCase): """Perform validation tests for MailingForm.""" From 6eb860579aefd4d271144a44fc25d38df6632a14 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 4 Jan 2025 17:50:14 +0100 Subject: [PATCH 11/40] split migrations --- ...012_club_board_group_club_members_group.py | 25 ------------- ...group_alter_club_members_group_and_more.py | 36 +++++++++++++++++++ ..._metagroup_alter_group_options_and_more.py | 22 +++--------- ...rt_is_manually_manageable_20250104_1742.py | 27 ++++++++++++++ 4 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 club/migrations/0013_alter_club_board_group_alter_club_members_group_and_more.py create mode 100644 core/migrations/0042_invert_is_manually_manageable_20250104_1742.py diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py index 2a93dd38..ef907fca 100644 --- a/club/migrations/0012_club_board_group_club_members_group.py +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -101,29 +101,4 @@ class Migration(migrations.Migration): migrations.RunPython( migrate_meta_groups, reverse_code=migrations.RunPython.noop, elidable=True ), - migrations.AlterField( - model_name="club", - name="board_group", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="club_board", - to="core.group", - ), - ), - migrations.AlterField( - model_name="club", - name="members_group", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="club", - to="core.group", - ), - ), - migrations.AddConstraint( - model_name="membership", - constraint=models.CheckConstraint( - check=models.Q(("end_date__gte", models.F("start_date"))), - name="end_after_start", - ), - ), ] diff --git a/club/migrations/0013_alter_club_board_group_alter_club_members_group_and_more.py b/club/migrations/0013_alter_club_board_group_alter_club_members_group_and_more.py new file mode 100644 index 00000000..21480bda --- /dev/null +++ b/club/migrations/0013_alter_club_board_group_alter_club_members_group_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.17 on 2025-01-04 16:46 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("club", "0012_club_board_group_club_members_group")] + + operations = [ + migrations.AlterField( + model_name="club", + name="board_group", + field=models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="club_board", + to="core.group", + ), + ), + migrations.AlterField( + model_name="club", + name="members_group", + field=models.OneToOneField( + on_delete=django.db.models.deletion.PROTECT, + related_name="club", + to="core.group", + ), + ), + migrations.AddConstraint( + model_name="membership", + constraint=models.CheckConstraint( + check=models.Q(("end_date__gte", models.F("start_date"))), + name="end_after_start", + ), + ), + ] diff --git a/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py b/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py index 2a88b8c1..8297ebf7 100644 --- a/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py +++ b/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py @@ -1,24 +1,13 @@ # Generated by Django 4.2.16 on 2024-11-30 13:16 from django.db import migrations, models -from django.db.migrations.state import StateApps -from django.db.models import F - - -def invert_is_manually_manageable(apps: StateApps, schema_editor): - """Invert `is_manually_manageable`. - - This field is a renaming of `is_meta`. - However, the meaning has been inverted : the groups - which were meta are not manually manageable and vice versa. - Thus, the value must be inverted. - """ - Group = apps.get_model("core", "Group") - Group.objects.all().update(is_manually_manageable=~F("is_manually_manageable")) class Migration(migrations.Migration): - dependencies = [("core", "0040_alter_user_options_user_user_permissions_and_more")] + dependencies = [ + ("core", "0040_alter_user_options_user_user_permissions_and_more"), + ("club", "0013_alter_club_board_group_alter_club_members_group_and_more"), + ] operations = [ migrations.DeleteModel( @@ -45,7 +34,4 @@ class Migration(migrations.Migration): verbose_name="Is manually manageable", ), ), - migrations.RunPython( - invert_is_manually_manageable, reverse_code=invert_is_manually_manageable - ), ] diff --git a/core/migrations/0042_invert_is_manually_manageable_20250104_1742.py b/core/migrations/0042_invert_is_manually_manageable_20250104_1742.py new file mode 100644 index 00000000..03eda90c --- /dev/null +++ b/core/migrations/0042_invert_is_manually_manageable_20250104_1742.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.17 on 2025-01-04 16:42 + +from django.db import migrations +from django.db.migrations.state import StateApps +from django.db.models import F + + +def invert_is_manually_manageable(apps: StateApps, schema_editor): + """Invert `is_manually_manageable`. + + This field is a renaming of `is_meta`. + However, the meaning has been inverted : the groups + which were meta are not manually manageable and vice versa. + Thus, the value must be inverted. + """ + Group = apps.get_model("core", "Group") + Group.objects.all().update(is_manually_manageable=~F("is_manually_manageable")) + + +class Migration(migrations.Migration): + dependencies = [("core", "0041_delete_metagroup_alter_group_options_and_more")] + + operations = [ + migrations.RunPython( + invert_is_manually_manageable, reverse_code=invert_is_manually_manageable + ), + ] From 849fac490de8d0078014a215109169ec6727b705 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 4 Jan 2025 18:49:00 +0100 Subject: [PATCH 12/40] fix get_or_create in club group migration --- club/migrations/0012_club_board_group_club_members_group.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/club/migrations/0012_club_board_group_club_members_group.py b/club/migrations/0012_club_board_group_club_members_group.py index ef907fca..f3d3a1e9 100644 --- a/club/migrations/0012_club_board_group_club_members_group.py +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -34,10 +34,12 @@ def migrate_meta_groups(apps: StateApps, schema_editor): clubs = list(Club.objects.all()) for club in clubs: club.board_group = meta_groups.get_or_create( - name=club.unix_name + settings.SITH_BOARD_SUFFIX + name=club.unix_name + settings.SITH_BOARD_SUFFIX, + defaults={"is_meta": True}, )[0] club.members_group = meta_groups.get_or_create( - name=club.unix_name + settings.SITH_MEMBER_SUFFIX + name=club.unix_name + settings.SITH_MEMBER_SUFFIX, + defaults={"is_meta": True}, )[0] club.save() club.refresh_from_db() From f0be4b270be07bea7b787d1eaa2350c53371919c Mon Sep 17 00:00:00 2001 From: imperosol Date: Sat, 4 Jan 2025 22:03:37 +0100 Subject: [PATCH 13/40] remove line numbers from locale files --- docs/howto/translation.md | 14 +- locale/fr/LC_MESSAGES/django.po | 3509 +++++++++++++---------------- locale/fr/LC_MESSAGES/djangojs.po | 84 +- 3 files changed, 1657 insertions(+), 1950 deletions(-) diff --git a/docs/howto/translation.md b/docs/howto/translation.md index 02f9f87b..122a1df3 100644 --- a/docs/howto/translation.md +++ b/docs/howto/translation.md @@ -38,10 +38,20 @@ l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serve ```bash # Pour le backend -./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules +./manage.py makemessages \ + --locale=fr \ + -e py,jinja \ + --ignore=node_modules \ + --add-location=file # Pour le frontend -./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules --ignore=staticfiles/generated +./manage.py makemessages \ + --locale=fr \ + -d djangojs \ + -e js,ts \ + --ignore=node_modules \ + --ignore=staticfiles/generated \ + --add-location=file ``` ## Éditer le fichier django.po diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 4b44dcf2..df9689e5 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-28 12:50+0100\n" +"POT-Creation-Date: 2025-01-04 21:59+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -16,229 +16,199 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132 -#: accounting/models.py:190 club/models.py:55 com/models.py:287 -#: com/models.py:306 counter/models.py:299 counter/models.py:332 -#: counter/models.py:483 forum/models.py:60 launderette/models.py:29 -#: launderette/models.py:80 launderette/models.py:116 +#: accounting/models.py club/models.py com/models.py counter/models.py +#: forum/models.py launderette/models.py msgid "name" msgstr "nom" -#: accounting/models.py:63 +#: accounting/models.py msgid "street" msgstr "rue" -#: accounting/models.py:64 +#: accounting/models.py msgid "city" msgstr "ville" -#: accounting/models.py:65 +#: accounting/models.py msgid "postcode" msgstr "code postal" -#: accounting/models.py:66 +#: accounting/models.py msgid "country" msgstr "pays" -#: accounting/models.py:67 core/models.py:279 +#: accounting/models.py core/models.py msgid "phone" msgstr "téléphone" -#: accounting/models.py:68 +#: accounting/models.py msgid "email" msgstr "email" -#: accounting/models.py:69 +#: accounting/models.py msgid "website" msgstr "site internet" -#: accounting/models.py:72 +#: accounting/models.py msgid "company" msgstr "entreprise" -#: accounting/models.py:102 +#: accounting/models.py msgid "iban" msgstr "IBAN" -#: accounting/models.py:103 +#: accounting/models.py msgid "account number" msgstr "numéro de compte" -#: accounting/models.py:107 accounting/models.py:136 club/models.py:356 -#: com/models.py:87 com/models.py:272 com/models.py:312 counter/models.py:361 -#: counter/models.py:485 trombi/models.py:209 +#: accounting/models.py club/models.py com/models.py counter/models.py +#: trombi/models.py msgid "club" msgstr "club" -#: accounting/models.py:112 +#: accounting/models.py msgid "Bank account" msgstr "Compte en banque" -#: accounting/models.py:142 +#: accounting/models.py msgid "bank account" msgstr "compte en banque" -#: accounting/models.py:147 +#: accounting/models.py msgid "Club account" msgstr "Compte club" -#: accounting/models.py:179 +#: accounting/models.py #, python-format msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:188 club/models.py:362 counter/models.py:1000 -#: election/models.py:16 launderette/models.py:165 +#: accounting/models.py club/models.py counter/models.py election/models.py +#: launderette/models.py msgid "start date" msgstr "date de début" -#: accounting/models.py:189 club/models.py:363 counter/models.py:1001 -#: election/models.py:17 +#: accounting/models.py club/models.py counter/models.py election/models.py msgid "end date" msgstr "date de fin" -#: accounting/models.py:191 +#: accounting/models.py msgid "is closed" msgstr "est fermé" -#: accounting/models.py:196 accounting/models.py:496 +#: accounting/models.py msgid "club account" msgstr "compte club" -#: accounting/models.py:199 accounting/models.py:255 counter/models.py:93 -#: counter/models.py:717 +#: accounting/models.py counter/models.py msgid "amount" msgstr "montant" -#: accounting/models.py:200 +#: accounting/models.py msgid "effective_amount" msgstr "montant effectif" -#: accounting/models.py:203 +#: accounting/models.py msgid "General journal" msgstr "Classeur" -#: accounting/models.py:247 +#: accounting/models.py msgid "number" msgstr "numéro" -#: accounting/models.py:252 +#: accounting/models.py msgid "journal" msgstr "classeur" -#: accounting/models.py:256 core/models.py:815 core/models.py:1324 -#: core/models.py:1369 core/models.py:1398 core/models.py:1422 -#: counter/models.py:727 counter/models.py:832 counter/models.py:1036 -#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 -#: forum/models.py:413 +#: accounting/models.py core/models.py counter/models.py eboutic/models.py +#: forum/models.py msgid "date" msgstr "date" -#: accounting/models.py:257 counter/models.py:302 counter/models.py:1037 -#: pedagogy/models.py:208 +#: accounting/models.py counter/models.py pedagogy/models.py msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:729 counter/models.py:834 -#: subscription/models.py:56 +#: accounting/models.py counter/models.py subscription/models.py msgid "payment method" msgstr "méthode de paiement" -#: accounting/models.py:264 +#: accounting/models.py msgid "cheque number" msgstr "numéro de chèque" -#: accounting/models.py:269 eboutic/models.py:291 +#: accounting/models.py eboutic/models.py msgid "invoice" msgstr "facture" -#: accounting/models.py:274 +#: accounting/models.py msgid "is done" msgstr "est fait" -#: accounting/models.py:278 +#: accounting/models.py msgid "simple type" msgstr "type simplifié" -#: accounting/models.py:286 accounting/models.py:441 +#: accounting/models.py msgid "accounting type" msgstr "type comptable" -#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 -#: accounting/models.py:492 core/models.py:1397 core/models.py:1423 -#: counter/models.py:798 +#: accounting/models.py core/models.py counter/models.py msgid "label" msgstr "étiquette" -#: accounting/models.py:300 +#: accounting/models.py msgid "target type" msgstr "type de cible" -#: accounting/models.py:303 club/models.py:611 -#: club/templates/club/club_members.jinja:17 -#: club/templates/club/club_old_members.jinja:8 -#: club/templates/club/mailing.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:32 -#: counter/templates/counter/stats.jinja:21 -#: counter/templates/counter/stats.jinja:47 -#: counter/templates/counter/stats.jinja:69 -#: launderette/templates/launderette/launderette_admin.jinja:44 +#: accounting/models.py club/models.py club/templates/club/club_members.jinja +#: club/templates/club/club_old_members.jinja club/templates/club/mailing.jinja +#: counter/templates/counter/cash_summary_list.jinja +#: counter/templates/counter/stats.jinja +#: launderette/templates/launderette/launderette_admin.jinja msgid "User" msgstr "Utilisateur" -#: accounting/models.py:304 club/models.py:515 -#: club/templates/club/club_detail.jinja:12 -#: com/templates/com/mailing_admin.jinja:11 -#: com/templates/com/news_admin_list.jinja:23 -#: com/templates/com/news_admin_list.jinja:54 -#: com/templates/com/news_admin_list.jinja:87 -#: com/templates/com/news_admin_list.jinja:130 -#: com/templates/com/news_admin_list.jinja:175 -#: com/templates/com/news_admin_list.jinja:212 -#: com/templates/com/news_admin_list.jinja:251 -#: com/templates/com/news_admin_list.jinja:288 -#: com/templates/com/weekmail.jinja:18 com/templates/com/weekmail.jinja:47 -#: core/templates/core/user_clubs.jinja:15 -#: core/templates/core/user_clubs.jinja:46 -#: counter/templates/counter/invoices_call.jinja:23 -#: trombi/templates/trombi/edit_profile.jinja:15 -#: trombi/templates/trombi/edit_profile.jinja:22 -#: trombi/templates/trombi/export.jinja:51 -#: trombi/templates/trombi/export.jinja:55 -#: trombi/templates/trombi/user_profile.jinja:34 -#: trombi/templates/trombi/user_profile.jinja:38 +#: accounting/models.py club/models.py club/templates/club/club_detail.jinja +#: com/templates/com/mailing_admin.jinja +#: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja +#: core/templates/core/user_clubs.jinja +#: counter/templates/counter/invoices_call.jinja +#: trombi/templates/trombi/edit_profile.jinja +#: trombi/templates/trombi/export.jinja +#: trombi/templates/trombi/user_profile.jinja msgid "Club" msgstr "Club" -#: accounting/models.py:305 core/views/user.py:283 +#: accounting/models.py core/views/user.py msgid "Account" msgstr "Compte" -#: accounting/models.py:306 +#: accounting/models.py msgid "Company" msgstr "Entreprise" -#: accounting/models.py:307 core/models.py:226 sith/settings.py:424 +#: accounting/models.py core/models.py sith/settings.py msgid "Other" msgstr "Autre" -#: accounting/models.py:310 +#: accounting/models.py msgid "target id" msgstr "id de la cible" -#: accounting/models.py:312 +#: accounting/models.py msgid "target label" msgstr "nom de la cible" -#: accounting/models.py:317 +#: accounting/models.py msgid "linked operation" msgstr "opération liée" -#: accounting/models.py:349 +#: accounting/models.py msgid "The date must be set." msgstr "La date doit être indiquée." -#: accounting/models.py:353 +#: accounting/models.py #, python-format msgid "" "The date can not be before the start date of the journal, which is\n" @@ -247,16 +217,16 @@ msgstr "" "La date ne peut pas être avant la date de début du journal, qui est\n" "%(start_date)s." -#: accounting/models.py:363 +#: accounting/models.py msgid "Target does not exists" msgstr "La cible n'existe pas." -#: accounting/models.py:366 +#: accounting/models.py msgid "Please add a target label if you set no existing target" msgstr "" "Merci d'ajouter un nom de cible si vous ne spécifiez pas de cible existante" -#: accounting/models.py:371 +#: accounting/models.py msgid "" "You need to provide ether a simplified accounting type or a standard " "accounting type" @@ -264,410 +234,344 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:421 counter/models.py:342 pedagogy/models.py:41 +#: accounting/models.py counter/models.py pedagogy/models.py msgid "code" msgstr "code" -#: accounting/models.py:425 +#: accounting/models.py msgid "An accounting type code contains only numbers" msgstr "Un code comptable ne contient que des numéros" -#: accounting/models.py:431 +#: accounting/models.py msgid "movement type" msgstr "type de mouvement" -#: accounting/models.py:433 -#: accounting/templates/accounting/journal_statement_nature.jinja:9 -#: accounting/templates/accounting/journal_statement_person.jinja:12 -#: accounting/views.py:566 +#: accounting/models.py +#: accounting/templates/accounting/journal_statement_nature.jinja +#: accounting/templates/accounting/journal_statement_person.jinja +#: accounting/views.py msgid "Credit" msgstr "Crédit" -#: accounting/models.py:434 -#: accounting/templates/accounting/journal_statement_nature.jinja:28 -#: accounting/templates/accounting/journal_statement_person.jinja:40 -#: accounting/views.py:566 +#: accounting/models.py +#: accounting/templates/accounting/journal_statement_nature.jinja +#: accounting/templates/accounting/journal_statement_person.jinja +#: accounting/views.py msgid "Debit" msgstr "Débit" -#: accounting/models.py:435 +#: accounting/models.py msgid "Neutral" msgstr "Neutre" -#: accounting/models.py:464 +#: accounting/models.py msgid "simplified accounting types" msgstr "type simplifié" -#: accounting/models.py:469 +#: accounting/models.py msgid "simplified type" msgstr "type simplifié" -#: accounting/templates/accounting/accountingtype_list.jinja:4 -#: accounting/templates/accounting/accountingtype_list.jinja:16 +#: accounting/templates/accounting/accountingtype_list.jinja msgid "Accounting type list" msgstr "Liste des types comptable" -#: accounting/templates/accounting/accountingtype_list.jinja:10 -#: accounting/templates/accounting/bank_account_details.jinja:10 -#: accounting/templates/accounting/bank_account_list.jinja:10 -#: accounting/templates/accounting/club_account_details.jinja:10 -#: accounting/templates/accounting/journal_details.jinja:10 -#: accounting/templates/accounting/label_list.jinja:10 -#: accounting/templates/accounting/operation_edit.jinja:10 -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:10 -#: core/templates/core/user_tools.jinja:88 +#: accounting/templates/accounting/accountingtype_list.jinja +#: accounting/templates/accounting/bank_account_details.jinja +#: accounting/templates/accounting/bank_account_list.jinja +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/label_list.jinja +#: accounting/templates/accounting/operation_edit.jinja +#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja +#: core/templates/core/user_tools.jinja msgid "Accounting" msgstr "Comptabilité" -#: accounting/templates/accounting/accountingtype_list.jinja:11 +#: accounting/templates/accounting/accountingtype_list.jinja msgid "Accounting types" msgstr "Type comptable" -#: accounting/templates/accounting/accountingtype_list.jinja:14 +#: accounting/templates/accounting/accountingtype_list.jinja msgid "New accounting type" msgstr "Nouveau type comptable" -#: accounting/templates/accounting/accountingtype_list.jinja:23 -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:23 +#: accounting/templates/accounting/accountingtype_list.jinja +#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja msgid "There is no types in this website." msgstr "Il n'y a pas de types comptable dans ce site web." -#: accounting/templates/accounting/bank_account_details.jinja:4 -#: accounting/templates/accounting/bank_account_details.jinja:14 -#: core/templates/core/user_tools.jinja:101 +#: accounting/templates/accounting/bank_account_details.jinja +#: core/templates/core/user_tools.jinja msgid "Bank account: " msgstr "Compte en banque : " -#: accounting/templates/accounting/bank_account_details.jinja:16 -#: accounting/templates/accounting/bank_account_details.jinja:29 -#: accounting/templates/accounting/club_account_details.jinja:17 -#: accounting/templates/accounting/club_account_details.jinja:60 -#: accounting/templates/accounting/label_list.jinja:26 -#: club/templates/club/club_sellings.jinja:78 -#: club/templates/club/mailing.jinja:16 club/templates/club/mailing.jinja:25 -#: club/templates/club/mailing.jinja:43 -#: com/templates/com/mailing_admin.jinja:19 -#: com/templates/com/news_admin_list.jinja:41 -#: com/templates/com/news_admin_list.jinja:70 -#: com/templates/com/news_admin_list.jinja:117 -#: com/templates/com/news_admin_list.jinja:158 -#: com/templates/com/news_admin_list.jinja:199 -#: com/templates/com/news_admin_list.jinja:234 -#: com/templates/com/news_admin_list.jinja:275 -#: com/templates/com/news_admin_list.jinja:310 -#: com/templates/com/poster_edit.jinja:21 -#: com/templates/com/poster_edit.jinja:23 -#: com/templates/com/screen_edit.jinja:16 com/templates/com/weekmail.jinja:33 -#: com/templates/com/weekmail.jinja:62 core/templates/core/file_detail.jinja:25 -#: core/templates/core/file_detail.jinja:62 -#: core/templates/core/file_moderation.jinja:48 -#: core/templates/core/group_detail.jinja:26 -#: core/templates/core/group_list.jinja:25 core/templates/core/macros.jinja:104 -#: core/templates/core/macros.jinja:123 core/templates/core/page_prop.jinja:14 -#: core/templates/core/user_account_detail.jinja:41 -#: core/templates/core/user_account_detail.jinja:77 -#: core/templates/core/user_clubs.jinja:34 -#: core/templates/core/user_clubs.jinja:63 -#: core/templates/core/user_edit.jinja:62 -#: counter/templates/counter/fragments/create_student_card.jinja:25 -#: counter/templates/counter/last_ops.jinja:35 -#: counter/templates/counter/last_ops.jinja:65 -#: election/templates/election/election_detail.jinja:191 -#: forum/templates/forum/macros.jinja:21 -#: launderette/templates/launderette/launderette_admin.jinja:16 -#: launderette/views.py:208 pedagogy/templates/pedagogy/guide.jinja:99 -#: pedagogy/templates/pedagogy/guide.jinja:114 -#: pedagogy/templates/pedagogy/uv_detail.jinja:189 -#: sas/templates/sas/album.jinja:36 sas/templates/sas/moderation.jinja:18 -#: sas/templates/sas/picture.jinja:71 trombi/templates/trombi/detail.jinja:35 -#: trombi/templates/trombi/edit_profile.jinja:35 +#: accounting/templates/accounting/bank_account_details.jinja +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/label_list.jinja +#: club/templates/club/club_sellings.jinja club/templates/club/mailing.jinja +#: com/templates/com/mailing_admin.jinja +#: com/templates/com/news_admin_list.jinja com/templates/com/poster_edit.jinja +#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja +#: core/templates/core/file_detail.jinja +#: core/templates/core/file_moderation.jinja +#: core/templates/core/group_detail.jinja core/templates/core/group_list.jinja +#: core/templates/core/macros.jinja core/templates/core/page_prop.jinja +#: core/templates/core/user_account_detail.jinja +#: core/templates/core/user_clubs.jinja core/templates/core/user_edit.jinja +#: counter/templates/counter/fragments/create_student_card.jinja +#: counter/templates/counter/last_ops.jinja +#: election/templates/election/election_detail.jinja +#: forum/templates/forum/macros.jinja +#: launderette/templates/launderette/launderette_admin.jinja +#: launderette/views.py pedagogy/templates/pedagogy/guide.jinja +#: pedagogy/templates/pedagogy/uv_detail.jinja sas/templates/sas/album.jinja +#: sas/templates/sas/moderation.jinja sas/templates/sas/picture.jinja +#: trombi/templates/trombi/detail.jinja +#: trombi/templates/trombi/edit_profile.jinja msgid "Delete" msgstr "Supprimer" -#: accounting/templates/accounting/bank_account_details.jinja:18 -#: club/views.py:78 core/views/user.py:201 sas/templates/sas/picture.jinja:91 +#: accounting/templates/accounting/bank_account_details.jinja club/views.py +#: core/views/user.py sas/templates/sas/picture.jinja msgid "Infos" msgstr "Infos" -#: accounting/templates/accounting/bank_account_details.jinja:20 +#: accounting/templates/accounting/bank_account_details.jinja msgid "IBAN: " msgstr "IBAN : " -#: accounting/templates/accounting/bank_account_details.jinja:21 +#: accounting/templates/accounting/bank_account_details.jinja msgid "Number: " msgstr "Numéro : " -#: accounting/templates/accounting/bank_account_details.jinja:23 +#: accounting/templates/accounting/bank_account_details.jinja msgid "New club account" msgstr "Nouveau compte club" -#: accounting/templates/accounting/bank_account_details.jinja:27 -#: accounting/templates/accounting/bank_account_list.jinja:22 -#: accounting/templates/accounting/club_account_details.jinja:58 -#: accounting/templates/accounting/journal_details.jinja:92 club/views.py:124 -#: com/templates/com/news_admin_list.jinja:39 -#: com/templates/com/news_admin_list.jinja:68 -#: com/templates/com/news_admin_list.jinja:115 -#: com/templates/com/news_admin_list.jinja:156 -#: com/templates/com/news_admin_list.jinja:197 -#: com/templates/com/news_admin_list.jinja:232 -#: com/templates/com/news_admin_list.jinja:273 -#: com/templates/com/news_admin_list.jinja:308 -#: com/templates/com/poster_list.jinja:43 -#: com/templates/com/poster_list.jinja:45 -#: com/templates/com/screen_list.jinja:26 com/templates/com/weekmail.jinja:32 -#: com/templates/com/weekmail.jinja:61 core/templates/core/file.jinja:42 -#: core/templates/core/group_list.jinja:24 core/templates/core/page.jinja:35 -#: core/templates/core/poster_list.jinja:40 -#: core/templates/core/user_tools.jinja:71 core/views/user.py:231 -#: counter/templates/counter/cash_summary_list.jinja:53 -#: counter/templates/counter/counter_list.jinja:17 -#: counter/templates/counter/counter_list.jinja:33 -#: counter/templates/counter/counter_list.jinja:49 -#: election/templates/election/election_detail.jinja:188 -#: forum/templates/forum/macros.jinja:20 forum/templates/forum/macros.jinja:62 -#: launderette/templates/launderette/launderette_list.jinja:16 -#: pedagogy/templates/pedagogy/guide.jinja:98 -#: pedagogy/templates/pedagogy/guide.jinja:113 -#: pedagogy/templates/pedagogy/uv_detail.jinja:188 -#: sas/templates/sas/album.jinja:35 trombi/templates/trombi/detail.jinja:9 -#: trombi/templates/trombi/edit_profile.jinja:34 +#: accounting/templates/accounting/bank_account_details.jinja +#: accounting/templates/accounting/bank_account_list.jinja +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja club/views.py +#: com/templates/com/news_admin_list.jinja com/templates/com/poster_list.jinja +#: com/templates/com/screen_list.jinja com/templates/com/weekmail.jinja +#: core/templates/core/file.jinja core/templates/core/group_list.jinja +#: core/templates/core/page.jinja core/templates/core/poster_list.jinja +#: core/templates/core/user_tools.jinja core/views/user.py +#: counter/templates/counter/cash_summary_list.jinja +#: counter/templates/counter/counter_list.jinja +#: election/templates/election/election_detail.jinja +#: forum/templates/forum/macros.jinja +#: launderette/templates/launderette/launderette_list.jinja +#: pedagogy/templates/pedagogy/guide.jinja +#: pedagogy/templates/pedagogy/uv_detail.jinja sas/templates/sas/album.jinja +#: trombi/templates/trombi/detail.jinja +#: trombi/templates/trombi/edit_profile.jinja msgid "Edit" msgstr "Éditer" -#: accounting/templates/accounting/bank_account_list.jinja:4 -#: accounting/templates/accounting/bank_account_list.jinja:18 +#: accounting/templates/accounting/bank_account_list.jinja msgid "Bank account list" msgstr "Liste des comptes en banque" -#: accounting/templates/accounting/bank_account_list.jinja:13 +#: accounting/templates/accounting/bank_account_list.jinja msgid "Manage simplified types" msgstr "Gérer les types simplifiés" -#: accounting/templates/accounting/bank_account_list.jinja:14 +#: accounting/templates/accounting/bank_account_list.jinja msgid "Manage accounting types" msgstr "Gérer les types comptable" -#: accounting/templates/accounting/bank_account_list.jinja:15 +#: accounting/templates/accounting/bank_account_list.jinja msgid "New bank account" msgstr "Nouveau compte en banque" -#: accounting/templates/accounting/bank_account_list.jinja:27 +#: accounting/templates/accounting/bank_account_list.jinja msgid "There is no accounts in this website." msgstr "Il n'y a pas de comptes dans ce site web." -#: accounting/templates/accounting/club_account_details.jinja:4 -#: accounting/templates/accounting/club_account_details.jinja:15 +#: accounting/templates/accounting/club_account_details.jinja msgid "Club account:" msgstr "Compte club : " -#: accounting/templates/accounting/club_account_details.jinja:20 -#: accounting/templates/accounting/journal_details.jinja:17 -#: accounting/templates/accounting/label_list.jinja:17 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/label_list.jinja msgid "New label" msgstr "Nouvelle étiquette" -#: accounting/templates/accounting/club_account_details.jinja:22 -#: accounting/templates/accounting/journal_details.jinja:18 -#: accounting/templates/accounting/label_list.jinja:4 -#: accounting/templates/accounting/label_list.jinja:20 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/label_list.jinja msgid "Label list" msgstr "Liste des étiquettes" -#: accounting/templates/accounting/club_account_details.jinja:24 +#: accounting/templates/accounting/club_account_details.jinja msgid "New journal" msgstr "Nouveau classeur" -#: accounting/templates/accounting/club_account_details.jinja:26 +#: accounting/templates/accounting/club_account_details.jinja msgid "You can not create new journal while you still have one opened" msgstr "Vous ne pouvez pas créer de journal tant qu'il y en a un d'ouvert" -#: accounting/templates/accounting/club_account_details.jinja:31 -#: launderette/templates/launderette/launderette_admin.jinja:43 +#: accounting/templates/accounting/club_account_details.jinja +#: launderette/templates/launderette/launderette_admin.jinja msgid "Name" msgstr "Nom" -#: accounting/templates/accounting/club_account_details.jinja:32 -#: com/templates/com/news_admin_list.jinja:178 -#: com/templates/com/news_admin_list.jinja:214 -#: com/templates/com/news_admin_list.jinja:254 -#: com/templates/com/news_admin_list.jinja:290 +#: accounting/templates/accounting/club_account_details.jinja +#: com/templates/com/news_admin_list.jinja msgid "Start" msgstr "Début" -#: accounting/templates/accounting/club_account_details.jinja:33 -#: com/templates/com/news_admin_list.jinja:179 -#: com/templates/com/news_admin_list.jinja:215 -#: com/templates/com/news_admin_list.jinja:255 -#: com/templates/com/news_admin_list.jinja:291 +#: accounting/templates/accounting/club_account_details.jinja +#: com/templates/com/news_admin_list.jinja msgid "End" msgstr "Fin" -#: accounting/templates/accounting/club_account_details.jinja:34 -#: accounting/templates/accounting/journal_details.jinja:35 -#: core/templates/core/user_account_detail.jinja:58 -#: core/templates/core/user_account_detail.jinja:93 -#: counter/templates/counter/last_ops.jinja:23 -#: counter/templates/counter/refilling_list.jinja:13 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/last_ops.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Amount" msgstr "Montant" -#: accounting/templates/accounting/club_account_details.jinja:35 +#: accounting/templates/accounting/club_account_details.jinja msgid "Effective amount" msgstr "Montant effectif" -#: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:462 +#: accounting/templates/accounting/club_account_details.jinja sith/settings.py msgid "Closed" msgstr "Fermé" -#: accounting/templates/accounting/club_account_details.jinja:37 -#: accounting/templates/accounting/journal_details.jinja:43 -#: com/templates/com/mailing_admin.jinja:12 -#: com/templates/com/news_admin_list.jinja:26 -#: com/templates/com/news_admin_list.jinja:56 -#: com/templates/com/news_admin_list.jinja:91 -#: com/templates/com/news_admin_list.jinja:133 -#: com/templates/com/news_admin_list.jinja:180 -#: com/templates/com/news_admin_list.jinja:216 -#: com/templates/com/news_admin_list.jinja:256 -#: com/templates/com/news_admin_list.jinja:292 -#: com/templates/com/weekmail.jinja:21 com/templates/com/weekmail.jinja:50 -#: counter/templates/counter/refilling_list.jinja:17 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja +#: com/templates/com/mailing_admin.jinja +#: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Actions" msgstr "Actions" -#: accounting/templates/accounting/club_account_details.jinja:53 -#: accounting/templates/accounting/journal_details.jinja:67 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja msgid "Yes" msgstr "Oui" -#: accounting/templates/accounting/club_account_details.jinja:55 -#: accounting/templates/accounting/journal_details.jinja:69 +#: accounting/templates/accounting/club_account_details.jinja +#: accounting/templates/accounting/journal_details.jinja msgid "No" msgstr "Non" -#: accounting/templates/accounting/club_account_details.jinja:57 -#: com/templates/com/news_admin_list.jinja:38 -#: com/templates/com/news_admin_list.jinja:67 -#: com/templates/com/news_admin_list.jinja:114 -#: com/templates/com/news_admin_list.jinja:155 -#: com/templates/com/news_admin_list.jinja:196 -#: com/templates/com/news_admin_list.jinja:231 -#: com/templates/com/news_admin_list.jinja:272 -#: com/templates/com/news_admin_list.jinja:307 -#: core/templates/core/file.jinja:40 core/templates/core/page.jinja:31 +#: accounting/templates/accounting/club_account_details.jinja +#: com/templates/com/news_admin_list.jinja core/templates/core/file.jinja +#: core/templates/core/page.jinja msgid "View" msgstr "Voir" -#: accounting/templates/accounting/co_list.jinja:4 -#: accounting/templates/accounting/journal_details.jinja:19 -#: core/templates/core/user_tools.jinja:95 +#: accounting/templates/accounting/co_list.jinja +#: accounting/templates/accounting/journal_details.jinja +#: core/templates/core/user_tools.jinja msgid "Company list" msgstr "Liste des entreprises" -#: accounting/templates/accounting/co_list.jinja:12 +#: accounting/templates/accounting/co_list.jinja msgid "Create new company" msgstr "Nouvelle entreprise" -#: accounting/templates/accounting/co_list.jinja:18 +#: accounting/templates/accounting/co_list.jinja msgid "Companies" msgstr "Entreprises" -#: accounting/templates/accounting/journal_details.jinja:4 -#: accounting/templates/accounting/journal_details.jinja:16 -#: accounting/templates/accounting/journal_statement_accounting.jinja:4 -#: accounting/templates/accounting/journal_statement_nature.jinja:4 -#: accounting/templates/accounting/journal_statement_person.jinja:4 +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/journal_statement_accounting.jinja +#: accounting/templates/accounting/journal_statement_nature.jinja +#: accounting/templates/accounting/journal_statement_person.jinja msgid "General journal:" msgstr "Classeur : " -#: accounting/templates/accounting/journal_details.jinja:20 -#: accounting/templates/accounting/journal_statement_accounting.jinja:30 -#: core/templates/core/user_account.jinja:39 -#: core/templates/core/user_account_detail.jinja:9 -#: counter/templates/counter/counter_click.jinja:39 +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/journal_statement_accounting.jinja +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/counter_click.jinja msgid "Amount: " msgstr "Montant : " -#: accounting/templates/accounting/journal_details.jinja:21 -#: accounting/templates/accounting/journal_statement_accounting.jinja:31 +#: accounting/templates/accounting/journal_details.jinja +#: accounting/templates/accounting/journal_statement_accounting.jinja msgid "Effective amount: " msgstr "Montant effectif: " -#: accounting/templates/accounting/journal_details.jinja:23 +#: accounting/templates/accounting/journal_details.jinja msgid "Journal is closed, you can not create operation" msgstr "Le classeur est fermé, vous ne pouvez pas créer d'opération" -#: accounting/templates/accounting/journal_details.jinja:25 +#: accounting/templates/accounting/journal_details.jinja msgid "New operation" msgstr "Nouvelle opération" -#: accounting/templates/accounting/journal_details.jinja:32 +#: accounting/templates/accounting/journal_details.jinja msgid "Nb" msgstr "No" -#: accounting/templates/accounting/journal_details.jinja:33 -#: club/templates/club/club_sellings.jinja:48 -#: core/templates/core/user_account_detail.jinja:16 -#: core/templates/core/user_account_detail.jinja:55 -#: core/templates/core/user_account_detail.jinja:91 -#: counter/templates/counter/cash_summary_list.jinja:34 -#: counter/templates/counter/last_ops.jinja:20 -#: counter/templates/counter/last_ops.jinja:45 -#: counter/templates/counter/refilling_list.jinja:16 -#: rootplace/templates/rootplace/logs.jinja:12 sas/forms.py:82 -#: trombi/templates/trombi/user_profile.jinja:40 +#: accounting/templates/accounting/journal_details.jinja +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/cash_summary_list.jinja +#: counter/templates/counter/last_ops.jinja +#: counter/templates/counter/refilling_list.jinja +#: rootplace/templates/rootplace/logs.jinja sas/forms.py +#: trombi/templates/trombi/user_profile.jinja msgid "Date" msgstr "Date" -#: accounting/templates/accounting/journal_details.jinja:34 -#: club/templates/club/club_sellings.jinja:52 -#: core/templates/core/user_account_detail.jinja:19 -#: counter/templates/counter/last_ops.jinja:48 -#: rootplace/templates/rootplace/logs.jinja:14 +#: accounting/templates/accounting/journal_details.jinja +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/last_ops.jinja +#: rootplace/templates/rootplace/logs.jinja msgid "Label" msgstr "Étiquette" -#: accounting/templates/accounting/journal_details.jinja:36 +#: accounting/templates/accounting/journal_details.jinja msgid "Payment mode" msgstr "Méthode de paiement" -#: accounting/templates/accounting/journal_details.jinja:37 +#: accounting/templates/accounting/journal_details.jinja msgid "Target" msgstr "Cible" -#: accounting/templates/accounting/journal_details.jinja:38 +#: accounting/templates/accounting/journal_details.jinja msgid "Code" msgstr "Code" -#: accounting/templates/accounting/journal_details.jinja:39 +#: accounting/templates/accounting/journal_details.jinja msgid "Nature" msgstr "Nature" -#: accounting/templates/accounting/journal_details.jinja:40 +#: accounting/templates/accounting/journal_details.jinja msgid "Done" msgstr "Effectuées" -#: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 -#: counter/views/cash.py:87 pedagogy/templates/pedagogy/moderation.jinja:13 -#: pedagogy/templates/pedagogy/uv_detail.jinja:142 -#: trombi/templates/trombi/comment.jinja:4 -#: trombi/templates/trombi/comment.jinja:8 -#: trombi/templates/trombi/user_tools.jinja:51 +#: accounting/templates/accounting/journal_details.jinja +#: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py +#: pedagogy/templates/pedagogy/moderation.jinja +#: pedagogy/templates/pedagogy/uv_detail.jinja +#: trombi/templates/trombi/comment.jinja +#: trombi/templates/trombi/user_tools.jinja msgid "Comment" msgstr "Commentaire" -#: accounting/templates/accounting/journal_details.jinja:42 +#: accounting/templates/accounting/journal_details.jinja msgid "File" msgstr "Fichier" -#: accounting/templates/accounting/journal_details.jinja:44 +#: accounting/templates/accounting/journal_details.jinja msgid "PDF" msgstr "PDF" -#: accounting/templates/accounting/journal_details.jinja:74 +#: accounting/templates/accounting/journal_details.jinja msgid "" "Warning: this operation has no linked operation because the targeted club " "account has no opened journal." @@ -675,7 +579,7 @@ msgstr "" "Attention: cette opération n'a pas d'opération liée parce qu'il n'y a pas de " "classeur ouvert dans le compte club cible" -#: accounting/templates/accounting/journal_details.jinja:77 +#: accounting/templates/accounting/journal_details.jinja #, python-format msgid "" "Open a journal in this club account, then save this " @@ -684,68 +588,61 @@ msgstr "" "Ouvrez un classeur dans ce compte club, puis sauver " "cette opération à nouveau pour créer l'opération liée." -#: accounting/templates/accounting/journal_details.jinja:96 +#: accounting/templates/accounting/journal_details.jinja msgid "Generate" msgstr "Générer" -#: accounting/templates/accounting/journal_statement_accounting.jinja:10 +#: accounting/templates/accounting/journal_statement_accounting.jinja msgid "Accounting statement: " msgstr "Bilan comptable : " -#: accounting/templates/accounting/journal_statement_accounting.jinja:15 -#: rootplace/templates/rootplace/logs.jinja:13 +#: accounting/templates/accounting/journal_statement_accounting.jinja +#: rootplace/templates/rootplace/logs.jinja msgid "Operation type" msgstr "Type d'opération" -#: accounting/templates/accounting/journal_statement_accounting.jinja:16 -#: accounting/templates/accounting/journal_statement_nature.jinja:14 -#: accounting/templates/accounting/journal_statement_nature.jinja:33 -#: accounting/templates/accounting/journal_statement_person.jinja:18 -#: accounting/templates/accounting/journal_statement_person.jinja:46 -#: counter/templates/counter/invoices_call.jinja:24 +#: accounting/templates/accounting/journal_statement_accounting.jinja +#: accounting/templates/accounting/journal_statement_nature.jinja +#: accounting/templates/accounting/journal_statement_person.jinja +#: counter/templates/counter/invoices_call.jinja msgid "Sum" msgstr "Somme" -#: accounting/templates/accounting/journal_statement_nature.jinja:13 -#: accounting/templates/accounting/journal_statement_nature.jinja:32 +#: accounting/templates/accounting/journal_statement_nature.jinja msgid "Nature of operation" msgstr "Nature de l'opération" -#: accounting/templates/accounting/journal_statement_nature.jinja:26 -#: accounting/templates/accounting/journal_statement_nature.jinja:45 -#: club/templates/club/club_sellings.jinja:42 -#: counter/templates/counter/counter_main.jinja:33 +#: accounting/templates/accounting/journal_statement_nature.jinja +#: club/templates/club/club_sellings.jinja +#: counter/templates/counter/counter_main.jinja msgid "Total: " msgstr "Total : " -#: accounting/templates/accounting/journal_statement_nature.jinja:49 +#: accounting/templates/accounting/journal_statement_nature.jinja msgid "Statement by nature: " msgstr "Bilan par nature : " -#: accounting/templates/accounting/journal_statement_person.jinja:10 +#: accounting/templates/accounting/journal_statement_person.jinja msgid "Statement by person: " msgstr "Bilan par personne : " -#: accounting/templates/accounting/journal_statement_person.jinja:17 -#: accounting/templates/accounting/journal_statement_person.jinja:45 +#: accounting/templates/accounting/journal_statement_person.jinja msgid "Target of the operation" msgstr "Cible de l'opération" -#: accounting/templates/accounting/label_list.jinja:15 +#: accounting/templates/accounting/label_list.jinja msgid "Back to club account" msgstr "Retour au compte club" -#: accounting/templates/accounting/label_list.jinja:32 +#: accounting/templates/accounting/label_list.jinja msgid "There is no label in this club account." msgstr "Il n'y a pas d'étiquette dans ce compte club." -#: accounting/templates/accounting/operation_edit.jinja:4 -#: accounting/templates/accounting/operation_edit.jinja:14 -#: accounting/templates/accounting/operation_edit.jinja:17 +#: accounting/templates/accounting/operation_edit.jinja msgid "Edit operation" msgstr "Éditer l'opération" -#: accounting/templates/accounting/operation_edit.jinja:26 +#: accounting/templates/accounting/operation_edit.jinja msgid "" "Warning: if you select Account, the opposite operation will be " "created in the target account. If you don't want that, select Club " @@ -755,253 +652,241 @@ msgstr "" "créée dans le compte cible. Si vous ne le voulez pas, sélectionnez Club à la place de Compte." -#: accounting/templates/accounting/operation_edit.jinja:47 +#: accounting/templates/accounting/operation_edit.jinja msgid "Linked operation:" msgstr "Opération liée : " -#: accounting/templates/accounting/operation_edit.jinja:55 -#: com/templates/com/news_edit.jinja:103 com/templates/com/poster_edit.jinja:33 -#: com/templates/com/screen_edit.jinja:25 com/templates/com/weekmail.jinja:74 -#: core/templates/core/create.jinja:12 core/templates/core/edit.jinja:7 -#: core/templates/core/edit.jinja:15 core/templates/core/edit.jinja:20 -#: core/templates/core/file_edit.jinja:8 -#: core/templates/core/macros_pages.jinja:26 -#: core/templates/core/page_prop.jinja:11 -#: core/templates/core/user_godfathers.jinja:61 -#: core/templates/core/user_godfathers_tree.jinja:85 -#: core/templates/core/user_preferences.jinja:18 -#: core/templates/core/user_preferences.jinja:27 -#: counter/templates/counter/cash_register_summary.jinja:28 -#: forum/templates/forum/reply.jinja:39 -#: subscription/templates/subscription/fragments/creation_form.jinja:9 -#: trombi/templates/trombi/comment.jinja:26 -#: trombi/templates/trombi/edit_profile.jinja:13 -#: trombi/templates/trombi/user_tools.jinja:13 +#: accounting/templates/accounting/operation_edit.jinja +#: com/templates/com/news_edit.jinja com/templates/com/poster_edit.jinja +#: com/templates/com/screen_edit.jinja com/templates/com/weekmail.jinja +#: core/templates/core/create.jinja core/templates/core/edit.jinja +#: core/templates/core/file_edit.jinja core/templates/core/macros_pages.jinja +#: core/templates/core/page_prop.jinja +#: core/templates/core/user_godfathers.jinja +#: core/templates/core/user_godfathers_tree.jinja +#: core/templates/core/user_preferences.jinja +#: counter/templates/counter/cash_register_summary.jinja +#: forum/templates/forum/reply.jinja +#: subscription/templates/subscription/fragments/creation_form.jinja +#: trombi/templates/trombi/comment.jinja +#: trombi/templates/trombi/edit_profile.jinja +#: trombi/templates/trombi/user_tools.jinja msgid "Save" msgstr "Sauver" -#: accounting/templates/accounting/refound_account.jinja:4 -#: accounting/templates/accounting/refound_account.jinja:9 -#: accounting/views.py:884 +#: accounting/templates/accounting/refound_account.jinja accounting/views.py msgid "Refound account" msgstr "Remboursement de compte" -#: accounting/templates/accounting/refound_account.jinja:13 +#: accounting/templates/accounting/refound_account.jinja msgid "Refound" msgstr "Rembourser" -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:4 -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:16 +#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja msgid "Simplified type list" msgstr "Liste des types simplifiés" -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:11 +#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja msgid "Simplified types" msgstr "Types simplifiés" -#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja:14 +#: accounting/templates/accounting/simplifiedaccountingtype_list.jinja msgid "New simplified type" msgstr "Nouveau type simplifié" -#: accounting/views.py:215 accounting/views.py:224 accounting/views.py:541 +#: accounting/views.py msgid "Journal" msgstr "Classeur" -#: accounting/views.py:232 +#: accounting/views.py msgid "Statement by nature" msgstr "Bilan par nature" -#: accounting/views.py:240 +#: accounting/views.py msgid "Statement by person" msgstr "Bilan par personne" -#: accounting/views.py:248 +#: accounting/views.py msgid "Accounting statement" msgstr "Bilan comptable" -#: accounting/views.py:361 +#: accounting/views.py msgid "Link this operation to the target account" msgstr "Lier cette opération au compte cible" -#: accounting/views.py:391 +#: accounting/views.py msgid "The target must be set." msgstr "La cible doit être indiquée." -#: accounting/views.py:406 +#: accounting/views.py msgid "The amount must be set." msgstr "Le montant doit être indiqué." -#: accounting/views.py:535 accounting/views.py:541 +#: accounting/views.py msgid "Operation" msgstr "Opération" -#: accounting/views.py:550 +#: accounting/views.py msgid "Financial proof: " msgstr "Justificatif de libellé : " -#: accounting/views.py:553 +#: accounting/views.py #, python-format msgid "Club: %(club_name)s" msgstr "Club : %(club_name)s" -#: accounting/views.py:558 +#: accounting/views.py #, python-format msgid "Label: %(op_label)s" msgstr "Libellé : %(op_label)s" -#: accounting/views.py:561 +#: accounting/views.py #, python-format msgid "Date: %(date)s" msgstr "Date : %(date)s" -#: accounting/views.py:569 +#: accounting/views.py #, python-format msgid "Amount: %(amount).2f €" msgstr "Montant : %(amount).2f €" -#: accounting/views.py:584 +#: accounting/views.py msgid "Debtor" msgstr "Débiteur" -#: accounting/views.py:584 +#: accounting/views.py msgid "Creditor" msgstr "Créditeur" -#: accounting/views.py:589 +#: accounting/views.py msgid "Comment:" msgstr "Commentaire :" -#: accounting/views.py:614 +#: accounting/views.py msgid "Signature:" msgstr "Signature :" -#: accounting/views.py:678 +#: accounting/views.py msgid "General statement" msgstr "Bilan général" -#: accounting/views.py:685 +#: accounting/views.py msgid "No label operations" msgstr "Opérations sans étiquette" -#: accounting/views.py:838 +#: accounting/views.py msgid "Refound this account" msgstr "Rembourser ce compte" -#: antispam/forms.py:18 +#: antispam/forms.py msgid "Email domain is not allowed." msgstr "Le domaine de l'addresse e-mail n'est pas autorisé." -#: antispam/models.py:8 +#: antispam/models.py msgid "domain" msgstr "domaine" -#: antispam/models.py:11 +#: antispam/models.py msgid "is externally managed" msgstr "est géré de manière externe" -#: antispam/models.py:14 +#: antispam/models.py msgid "" "True if kept up-to-date using external toxic domain providers, else False" msgstr "" "True si gardé à jour par le biais d'un fournisseur externe de domains " "toxics, False sinon" -#: club/forms.py:54 club/forms.py:180 +#: club/forms.py msgid "Users to add" msgstr "Utilisateurs à ajouter" -#: club/forms.py:55 club/forms.py:181 core/views/group.py:40 +#: club/forms.py core/views/group.py msgid "Search users to add (one or more)." msgstr "Recherche les utilisateurs à ajouter (un ou plus)." -#: club/forms.py:66 +#: club/forms.py msgid "New Mailing" msgstr "Nouvelle mailing liste" -#: club/forms.py:67 +#: club/forms.py msgid "Subscribe" msgstr "S'abonner" -#: club/forms.py:68 club/forms.py:81 com/templates/com/news_admin_list.jinja:40 -#: com/templates/com/news_admin_list.jinja:116 -#: com/templates/com/news_admin_list.jinja:198 -#: com/templates/com/news_admin_list.jinja:274 +#: club/forms.py com/templates/com/news_admin_list.jinja msgid "Remove" msgstr "Retirer" -#: club/forms.py:71 launderette/views.py:210 -#: pedagogy/templates/pedagogy/moderation.jinja:15 +#: club/forms.py launderette/views.py +#: pedagogy/templates/pedagogy/moderation.jinja msgid "Action" msgstr "Action" -#: club/forms.py:109 club/tests.py:742 +#: club/forms.py club/tests.py msgid "This field is required" msgstr "Ce champ est obligatoire" -#: club/forms.py:118 club/tests.py:773 +#: club/forms.py club/tests.py msgid "One of the selected users doesn't have an email address" msgstr "Un des utilisateurs sélectionnés n'a pas d'adresse email" -#: club/forms.py:129 +#: club/forms.py msgid "An action is required" msgstr "Une action est requise" -#: club/forms.py:140 club/tests.py:729 club/tests.py:755 +#: club/forms.py club/tests.py msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:209 +#: club/forms.py counter/forms.py msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:212 -#: election/views.py:170 subscription/forms.py:21 +#: club/forms.py com/views.py counter/forms.py election/views.py +#: subscription/forms.py msgid "End date" msgstr "Date de fin" -#: club/forms.py:156 club/templates/club/club_sellings.jinja:49 -#: core/templates/core/user_account_detail.jinja:17 -#: core/templates/core/user_account_detail.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:33 -#: counter/views/mixins.py:58 +#: club/forms.py club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/cash_summary_list.jinja counter/views/mixins.py msgid "Counter" msgstr "Comptoir" -#: club/forms.py:163 counter/views/mixins.py:94 +#: club/forms.py counter/views/mixins.py msgid "Products" msgstr "Produits" -#: club/forms.py:168 counter/templates/counter/product_list.jinja:43 +#: club/forms.py counter/templates/counter/product_list.jinja msgid "Archived products" msgstr "Produits archivés" -#: club/forms.py:224 club/templates/club/club_members.jinja:22 -#: club/templates/club/club_members.jinja:48 -#: core/templates/core/user_clubs.jinja:31 +#: club/forms.py club/templates/club/club_members.jinja +#: core/templates/core/user_clubs.jinja msgid "Mark as old" msgstr "Marquer comme ancien" -#: club/forms.py:241 +#: club/forms.py msgid "User must be subscriber to take part to a club" msgstr "L'utilisateur doit être cotisant pour faire partie d'un club" -#: club/forms.py:245 +#: club/forms.py msgid "You can not add the same user twice" msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur" -#: club/forms.py:264 +#: club/forms.py msgid "You should specify a role" msgstr "Vous devez choisir un rôle" -#: club/forms.py:275 sas/views.py:58 sas/views.py:176 +#: club/forms.py sas/views.py msgid "You do not have the permission to do that" msgstr "Vous n'avez pas la permission de faire cela" -#: club/models.py:60 +#: club/models.py msgid "unix name" msgstr "nom unix" -#: club/models.py:67 +#: club/models.py msgid "" "Enter a valid unix name. This value may contain only letters, numbers ./-/_ " "characters." @@ -1009,294 +894,273 @@ msgstr "" "Entrez un nom UNIX valide. Cette valeur peut contenir uniquement des " "lettres, des nombres, et les caractères ./-/_" -#: club/models.py:72 +#: club/models.py msgid "A club with that unix name already exists." msgstr "Un club avec ce nom UNIX existe déjà." -#: club/models.py:75 +#: club/models.py msgid "logo" msgstr "logo" -#: club/models.py:77 +#: club/models.py msgid "is active" msgstr "actif" -#: club/models.py:79 +#: club/models.py msgid "short description" msgstr "description courte" -#: club/models.py:81 core/models.py:281 +#: club/models.py core/models.py msgid "address" msgstr "Adresse" -#: club/models.py:98 core/models.py:192 +#: club/models.py core/models.py msgid "home" msgstr "home" -#: club/models.py:159 +#: club/models.py msgid "You can not make loops in clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs" -#: club/models.py:348 counter/models.py:991 counter/models.py:1027 -#: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 -#: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 -#: trombi/models.py:205 +#: club/models.py counter/models.py eboutic/models.py election/models.py +#: launderette/models.py sas/models.py trombi/models.py msgid "user" msgstr "utilisateur" -#: club/models.py:365 core/models.py:245 election/models.py:178 -#: election/models.py:212 trombi/models.py:210 +#: club/models.py core/models.py election/models.py trombi/models.py msgid "role" msgstr "rôle" -#: club/models.py:370 core/models.py:69 counter/models.py:300 -#: counter/models.py:333 election/models.py:13 election/models.py:115 -#: election/models.py:188 forum/models.py:61 forum/models.py:245 +#: club/models.py core/models.py counter/models.py election/models.py +#: forum/models.py msgid "description" msgstr "description" -#: club/models.py:522 club/models.py:617 +#: club/models.py msgid "Email address" msgstr "Adresse email" -#: club/models.py:530 +#: club/models.py msgid "Enter a valid address. Only the root of the address is needed." msgstr "" "Entrez une adresse valide. Seule la racine de l'adresse est nécessaire." -#: club/models.py:534 com/models.py:97 com/models.py:322 core/models.py:816 +#: club/models.py com/models.py core/models.py msgid "is moderated" msgstr "est modéré" -#: club/models.py:538 com/models.py:101 com/models.py:326 +#: club/models.py com/models.py msgid "moderator" msgstr "modérateur" -#: club/models.py:564 +#: club/models.py msgid "This mailing list already exists." msgstr "Cette liste de diffusion existe déjà." -#: club/models.py:603 club/templates/club/mailing.jinja:23 +#: club/models.py club/templates/club/mailing.jinja msgid "Mailing" msgstr "Liste de diffusion" -#: club/models.py:627 +#: club/models.py msgid "At least user or email is required" msgstr "Au moins un utilisateur ou un email est nécessaire" -#: club/models.py:635 club/tests.py:801 +#: club/models.py club/tests.py msgid "This email is already suscribed in this mailing" msgstr "Cet email est déjà abonné à cette mailing" -#: club/models.py:663 +#: club/models.py msgid "Unregistered user" msgstr "Utilisateur non enregistré" -#: club/templates/club/club_list.jinja:4 club/templates/club/club_list.jinja:37 +#: club/templates/club/club_list.jinja msgid "Club list" msgstr "Liste des clubs" -#: club/templates/club/club_list.jinja:14 +#: club/templates/club/club_list.jinja msgid "inactive" msgstr "inactif" -#: club/templates/club/club_list.jinja:34 -#: core/templates/core/user_tools.jinja:31 +#: club/templates/club/club_list.jinja core/templates/core/user_tools.jinja msgid "New club" msgstr "Nouveau club" -#: club/templates/club/club_list.jinja:44 club/templates/club/stats.jinja:44 +#: club/templates/club/club_list.jinja club/templates/club/stats.jinja msgid "There is no club in this website." msgstr "Il n'y a pas de club dans ce site web." -#: club/templates/club/club_members.jinja:5 +#: club/templates/club/club_members.jinja msgid "Club members" msgstr "Membres du club" -#: club/templates/club/club_members.jinja:18 -#: club/templates/club/club_old_members.jinja:9 -#: core/templates/core/user_clubs.jinja:16 -#: core/templates/core/user_clubs.jinja:47 -#: trombi/templates/trombi/edit_profile.jinja:23 -#: trombi/templates/trombi/export.jinja:56 -#: trombi/templates/trombi/user_profile.jinja:39 +#: club/templates/club/club_members.jinja +#: club/templates/club/club_old_members.jinja +#: core/templates/core/user_clubs.jinja +#: trombi/templates/trombi/edit_profile.jinja +#: trombi/templates/trombi/export.jinja +#: trombi/templates/trombi/user_profile.jinja msgid "Role" msgstr "Rôle" -#: club/templates/club/club_members.jinja:19 -#: club/templates/club/club_old_members.jinja:10 -#: core/templates/core/group_list.jinja:15 -#: core/templates/core/user_clubs.jinja:17 -#: core/templates/core/user_clubs.jinja:48 +#: club/templates/club/club_members.jinja +#: club/templates/club/club_old_members.jinja +#: core/templates/core/group_list.jinja core/templates/core/user_clubs.jinja msgid "Description" msgstr "Description" -#: club/templates/club/club_members.jinja:20 -#: core/templates/core/user_clubs.jinja:18 -#: launderette/templates/launderette/launderette_admin.jinja:45 +#: club/templates/club/club_members.jinja core/templates/core/user_clubs.jinja +#: launderette/templates/launderette/launderette_admin.jinja msgid "Since" msgstr "Depuis" -#: club/templates/club/club_members.jinja:52 +#: club/templates/club/club_members.jinja msgid "There are no members in this club." msgstr "Il n'y a pas de membres dans ce club." -#: club/templates/club/club_members.jinja:80 -#: core/templates/core/file_detail.jinja:19 core/views/forms.py:308 -#: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19 +#: club/templates/club/club_members.jinja core/templates/core/file_detail.jinja +#: core/views/forms.py launderette/views.py +#: trombi/templates/trombi/detail.jinja msgid "Add" msgstr "Ajouter" -#: club/templates/club/club_old_members.jinja:5 +#: club/templates/club/club_old_members.jinja msgid "Club old members" msgstr "Anciens membres du club" -#: club/templates/club/club_old_members.jinja:11 -#: core/templates/core/user_clubs.jinja:49 +#: club/templates/club/club_old_members.jinja +#: core/templates/core/user_clubs.jinja msgid "From" msgstr "Du" -#: club/templates/club/club_old_members.jinja:12 -#: core/templates/core/user_clubs.jinja:50 +#: club/templates/club/club_old_members.jinja +#: core/templates/core/user_clubs.jinja msgid "To" msgstr "Au" -#: club/templates/club/club_sellings.jinja:13 -#: club/templates/club/club_sellings.jinja:15 +#: club/templates/club/club_sellings.jinja msgid "Previous" msgstr "Précédent" -#: club/templates/club/club_sellings.jinja:19 +#: club/templates/club/club_sellings.jinja msgid "current" msgstr "actuel" -#: club/templates/club/club_sellings.jinja:25 -#: club/templates/club/club_sellings.jinja:27 +#: club/templates/club/club_sellings.jinja msgid "Next" msgstr "Suivant" -#: club/templates/club/club_sellings.jinja:33 -#: counter/templates/counter/counter_main.jinja:24 -#: counter/templates/counter/last_ops.jinja:41 +#: club/templates/club/club_sellings.jinja +#: counter/templates/counter/counter_main.jinja +#: counter/templates/counter/last_ops.jinja msgid "Sales" msgstr "Ventes" -#: club/templates/club/club_sellings.jinja:37 -#: club/templates/club/stats.jinja:19 -#: counter/templates/counter/cash_summary_list.jinja:15 +#: club/templates/club/club_sellings.jinja club/templates/club/stats.jinja +#: counter/templates/counter/cash_summary_list.jinja msgid "Show" msgstr "Montrer" -#: club/templates/club/club_sellings.jinja:38 -#: counter/templates/counter/product_list.jinja:74 +#: club/templates/club/club_sellings.jinja +#: counter/templates/counter/product_list.jinja msgid "Download as cvs" msgstr "Télécharger en CSV" -#: club/templates/club/club_sellings.jinja:41 +#: club/templates/club/club_sellings.jinja msgid "Quantity: " msgstr "Quantité : " -#: club/templates/club/club_sellings.jinja:41 +#: club/templates/club/club_sellings.jinja msgid "units" msgstr "unités" -#: club/templates/club/club_sellings.jinja:43 +#: club/templates/club/club_sellings.jinja msgid "Benefit: " msgstr "Bénéfice : " -#: club/templates/club/club_sellings.jinja:50 -#: core/templates/core/user_account_detail.jinja:18 -#: core/templates/core/user_account_detail.jinja:57 -#: counter/templates/counter/last_ops.jinja:21 -#: counter/templates/counter/last_ops.jinja:46 +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/last_ops.jinja msgid "Barman" msgstr "Barman" -#: club/templates/club/club_sellings.jinja:51 -#: counter/templates/counter/counter_click.jinja:36 -#: counter/templates/counter/last_ops.jinja:22 -#: counter/templates/counter/last_ops.jinja:47 -#: counter/templates/counter/refilling_list.jinja:12 +#: club/templates/club/club_sellings.jinja +#: counter/templates/counter/counter_click.jinja +#: counter/templates/counter/last_ops.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Customer" msgstr "Client" -#: club/templates/club/club_sellings.jinja:53 -#: core/templates/core/user_account_detail.jinja:20 -#: core/templates/core/user_stats.jinja:44 -#: counter/templates/counter/last_ops.jinja:49 +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: core/templates/core/user_stats.jinja +#: counter/templates/counter/last_ops.jinja msgid "Quantity" msgstr "Quantité" -#: club/templates/club/club_sellings.jinja:54 -#: core/templates/core/user_account.jinja:10 -#: core/templates/core/user_account_detail.jinja:21 -#: counter/templates/counter/cash_summary_list.jinja:35 -#: counter/templates/counter/last_ops.jinja:50 -#: counter/templates/counter/stats.jinja:23 -#: subscription/templates/subscription/stats.jinja:42 -#: subscription/templates/subscription/stats.jinja:50 +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/cash_summary_list.jinja +#: counter/templates/counter/last_ops.jinja +#: counter/templates/counter/stats.jinja +#: subscription/templates/subscription/stats.jinja msgid "Total" msgstr "Total" -#: club/templates/club/club_sellings.jinja:55 -#: core/templates/core/user_account_detail.jinja:22 -#: core/templates/core/user_account_detail.jinja:59 -#: core/templates/core/user_detail.jinja:186 -#: counter/templates/counter/last_ops.jinja:24 -#: counter/templates/counter/last_ops.jinja:51 -#: counter/templates/counter/refilling_list.jinja:14 +#: club/templates/club/club_sellings.jinja +#: core/templates/core/user_account_detail.jinja +#: core/templates/core/user_detail.jinja +#: counter/templates/counter/last_ops.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Payment method" msgstr "Méthode de paiement" -#: club/templates/club/club_tools.jinja:4 -#: core/templates/core/user_tools.jinja:149 +#: club/templates/club/club_tools.jinja core/templates/core/user_tools.jinja msgid "Club tools" msgstr "Outils club" -#: club/templates/club/club_tools.jinja:6 +#: club/templates/club/club_tools.jinja msgid "Communication:" msgstr "Communication : " -#: club/templates/club/club_tools.jinja:8 +#: club/templates/club/club_tools.jinja msgid "Create a news" msgstr "Créer une nouvelle" -#: club/templates/club/club_tools.jinja:9 +#: club/templates/club/club_tools.jinja msgid "Post in the Weekmail" msgstr "Poster dans le Weekmail" -#: club/templates/club/club_tools.jinja:11 +#: club/templates/club/club_tools.jinja msgid "Edit Trombi" msgstr "Éditer le Trombi" -#: club/templates/club/club_tools.jinja:13 +#: club/templates/club/club_tools.jinja msgid "New Trombi" msgstr "Nouveau Trombi" -#: club/templates/club/club_tools.jinja:14 -#: com/templates/com/poster_list.jinja:17 -#: core/templates/core/poster_list.jinja:17 -#: core/templates/core/user_tools.jinja:137 +#: club/templates/club/club_tools.jinja com/templates/com/poster_list.jinja +#: core/templates/core/poster_list.jinja core/templates/core/user_tools.jinja msgid "Posters" msgstr "Affiches" -#: club/templates/club/club_tools.jinja:17 +#: club/templates/club/club_tools.jinja msgid "Counters:" msgstr "Comptoirs : " -#: club/templates/club/club_tools.jinja:33 +#: club/templates/club/club_tools.jinja msgid "Accounting: " msgstr "Comptabilité : " -#: club/templates/club/club_tools.jinja:41 +#: club/templates/club/club_tools.jinja msgid "Manage launderettes" msgstr "Gestion des laveries" -#: club/templates/club/mailing.jinja:5 +#: club/templates/club/mailing.jinja msgid "Mailing lists" msgstr "Mailing listes" -#: club/templates/club/mailing.jinja:10 +#: club/templates/club/mailing.jinja msgid "" "Remember : mailing lists need to be moderated, if your new created list is " "not shown wait until moderation takes action" @@ -1305,136 +1169,130 @@ msgstr "" "nouvellement créée n'est pas affichée, attendez jusqu'à ce qu'un modérateur " "prenne une décision" -#: club/templates/club/mailing.jinja:13 +#: club/templates/club/mailing.jinja msgid "Mailing lists waiting for moderation" msgstr "Listes de diffusions en attente de modération" -#: club/templates/club/mailing.jinja:29 +#: club/templates/club/mailing.jinja msgid "Generate mailing list" msgstr "Générer la liste de diffusion" -#: club/templates/club/mailing.jinja:42 -#: com/templates/com/mailing_admin.jinja:10 +#: club/templates/club/mailing.jinja com/templates/com/mailing_admin.jinja msgid "Email" msgstr "Email" -#: club/templates/club/mailing.jinja:58 +#: club/templates/club/mailing.jinja msgid "Remove from mailing list" msgstr "Supprimer de la liste de diffusion" -#: club/templates/club/mailing.jinja:62 +#: club/templates/club/mailing.jinja msgid "There is no subscriber for this mailing list" msgstr "Il n'y a pas d'abonnés dans cette liste de diffusion" -#: club/templates/club/mailing.jinja:67 +#: club/templates/club/mailing.jinja msgid "No mailing list existing for this club" msgstr "Aucune mailing liste n'existe pour ce club" -#: club/templates/club/mailing.jinja:72 -#: subscription/templates/subscription/subscription.jinja:38 +#: club/templates/club/mailing.jinja +#: subscription/templates/subscription/subscription.jinja msgid "New member" msgstr "Nouveau membre" -#: club/templates/club/mailing.jinja:92 +#: club/templates/club/mailing.jinja msgid "Add to mailing list" msgstr "Ajouter à la mailing liste" -#: club/templates/club/mailing.jinja:96 +#: club/templates/club/mailing.jinja msgid "New mailing" msgstr "Nouvelle liste de diffusion" -#: club/templates/club/mailing.jinja:105 +#: club/templates/club/mailing.jinja msgid "Create mailing list" msgstr "Créer une liste de diffusion" -#: club/templates/club/page_history.jinja:8 +#: club/templates/club/page_history.jinja msgid "No page existing for this club" msgstr "Aucune page n'existe pour ce club" -#: club/templates/club/stats.jinja:4 club/templates/club/stats.jinja:9 +#: club/templates/club/stats.jinja msgid "Club stats" msgstr "Statistiques du club" -#: club/views.py:88 +#: club/views.py msgid "Members" msgstr "Membres" -#: club/views.py:97 +#: club/views.py msgid "Old members" msgstr "Anciens membres" -#: club/views.py:107 core/templates/core/page.jinja:33 +#: club/views.py core/templates/core/page.jinja msgid "History" msgstr "Historique" -#: club/views.py:115 core/templates/core/base/header.jinja:61 -#: core/views/user.py:224 sas/templates/sas/picture.jinja:110 -#: trombi/views.py:62 +#: club/views.py core/templates/core/base/header.jinja core/views/user.py +#: sas/templates/sas/picture.jinja trombi/views.py msgid "Tools" msgstr "Outils" -#: club/views.py:135 +#: club/views.py msgid "Edit club page" msgstr "Éditer la page de club" -#: club/views.py:144 club/views.py:451 +#: club/views.py msgid "Sellings" msgstr "Vente" -#: club/views.py:151 +#: club/views.py msgid "Mailing list" msgstr "Listes de diffusion" -#: club/views.py:160 com/views.py:134 +#: club/views.py com/views.py msgid "Posters list" msgstr "Liste d'affiches" -#: club/views.py:170 counter/templates/counter/counter_list.jinja:21 -#: counter/templates/counter/counter_list.jinja:37 -#: counter/templates/counter/counter_list.jinja:53 +#: club/views.py counter/templates/counter/counter_list.jinja msgid "Props" msgstr "Propriétés" -#: com/models.py:43 +#: com/models.py msgid "alert message" msgstr "message d'alerte" -#: com/models.py:44 +#: com/models.py msgid "info message" msgstr "message d'info" -#: com/models.py:45 +#: com/models.py msgid "weekmail destinations" msgstr "destinataires du weekmail" -#: com/models.py:57 +#: com/models.py msgid "Notice" msgstr "Information" -#: com/models.py:58 +#: com/models.py msgid "Event" msgstr "Événement" -#: com/models.py:59 +#: com/models.py msgid "Weekly" msgstr "Hebdomadaire" -#: com/models.py:60 +#: com/models.py msgid "Call" msgstr "Appel" -#: com/models.py:67 com/models.py:187 com/models.py:261 -#: core/templates/core/macros.jinja:301 election/models.py:12 -#: election/models.py:114 election/models.py:152 forum/models.py:256 -#: forum/models.py:310 pedagogy/models.py:97 +#: com/models.py core/templates/core/macros.jinja election/models.py +#: forum/models.py pedagogy/models.py msgid "title" msgstr "titre" -#: com/models.py:69 +#: com/models.py msgid "summary" msgstr "résumé" -#: com/models.py:71 +#: com/models.py msgid "" "A description of the event (what is the activity ? is there an associated " "clic ? is there a inscription form ?)" @@ -1442,276 +1300,234 @@ msgstr "" "Une description de l'évènement (quelle est l'activité ? Y a-t-il un clic " "associé ? Y-a-t'il un formulaire d'inscription ?)" -#: com/models.py:76 com/models.py:262 trombi/models.py:188 +#: com/models.py trombi/models.py msgid "content" msgstr "contenu" -#: com/models.py:79 +#: com/models.py msgid "A more detailed and exhaustive description of the event." msgstr "Une description plus détaillée et exhaustive de l'évènement." -#: com/models.py:82 core/models.py:1367 launderette/models.py:88 -#: launderette/models.py:124 launderette/models.py:167 +#: com/models.py core/models.py launderette/models.py msgid "type" msgstr "type" -#: com/models.py:89 +#: com/models.py msgid "The club which organizes the event." msgstr "Le club qui organise l'évènement." -#: com/models.py:94 com/models.py:266 pedagogy/models.py:57 -#: pedagogy/models.py:200 trombi/models.py:178 +#: com/models.py pedagogy/models.py trombi/models.py msgid "author" msgstr "auteur" -#: com/models.py:166 +#: com/models.py msgid "news_date" msgstr "date de la nouvelle" -#: com/models.py:169 +#: com/models.py msgid "start_date" msgstr "date de début" -#: com/models.py:170 +#: com/models.py msgid "end_date" msgstr "date de fin" -#: com/models.py:188 +#: com/models.py msgid "intro" msgstr "intro" -#: com/models.py:189 +#: com/models.py msgid "joke" msgstr "blague" -#: com/models.py:190 +#: com/models.py msgid "protip" msgstr "astuce" -#: com/models.py:191 +#: com/models.py msgid "conclusion" msgstr "conclusion" -#: com/models.py:192 +#: com/models.py msgid "sent" msgstr "envoyé" -#: com/models.py:257 +#: com/models.py msgid "weekmail" msgstr "weekmail" -#: com/models.py:275 +#: com/models.py msgid "rank" msgstr "rang" -#: com/models.py:308 core/models.py:781 core/models.py:831 +#: com/models.py core/models.py msgid "file" msgstr "fichier" -#: com/models.py:320 +#: com/models.py msgid "display time" msgstr "temps d'affichage" -#: com/models.py:349 +#: com/models.py msgid "Begin date should be before end date" msgstr "La date de début doit être avant celle de fin" -#: com/templates/com/mailing_admin.jinja:4 com/views.py:127 -#: core/templates/core/user_tools.jinja:136 +#: com/templates/com/mailing_admin.jinja com/views.py +#: core/templates/core/user_tools.jinja msgid "Mailing lists administration" msgstr "Administration des mailing listes" -#: com/templates/com/mailing_admin.jinja:19 -#: com/templates/com/news_admin_list.jinja:69 -#: com/templates/com/news_admin_list.jinja:157 -#: com/templates/com/news_admin_list.jinja:233 -#: com/templates/com/news_admin_list.jinja:309 -#: com/templates/com/news_detail.jinja:39 -#: core/templates/core/file_detail.jinja:65 -#: core/templates/core/file_moderation.jinja:42 -#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:68 +#: com/templates/com/mailing_admin.jinja +#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja +#: core/templates/core/file_detail.jinja +#: core/templates/core/file_moderation.jinja sas/templates/sas/moderation.jinja +#: sas/templates/sas/picture.jinja msgid "Moderate" msgstr "Modérer" -#: com/templates/com/mailing_admin.jinja:19 +#: com/templates/com/mailing_admin.jinja #, python-format msgid "Moderated by %(user)s" msgstr "Modéré par %(user)s" -#: com/templates/com/mailing_admin.jinja:28 +#: com/templates/com/mailing_admin.jinja msgid "This page lists all mailing lists" msgstr "Cette page liste toutes les listes de diffusion" -#: com/templates/com/mailing_admin.jinja:31 +#: com/templates/com/mailing_admin.jinja msgid "Not moderated mailing lists" msgstr "Listes de diffusion non modérées" -#: com/templates/com/mailing_admin.jinja:35 +#: com/templates/com/mailing_admin.jinja msgid "Moderated mailing lists" msgstr "Modérer les listes de diffusion" -#: com/templates/com/mailing_admin.jinja:39 +#: com/templates/com/mailing_admin.jinja msgid "No mailing list existing" msgstr "Aucune liste de diffusion existante" -#: com/templates/com/news_admin_list.jinja:5 +#: com/templates/com/news_admin_list.jinja msgid "News admin" msgstr "Administration des nouvelles" -#: com/templates/com/news_admin_list.jinja:9 -#: com/templates/com/news_detail.jinja:5 com/templates/com/news_list.jinja:5 +#: com/templates/com/news_admin_list.jinja com/templates/com/news_detail.jinja +#: com/templates/com/news_list.jinja msgid "News" msgstr "Nouvelles" -#: com/templates/com/news_admin_list.jinja:11 -#: com/templates/com/news_edit.jinja:8 com/templates/com/news_edit.jinja:31 -#: core/templates/core/user_tools.jinja:131 +#: com/templates/com/news_admin_list.jinja com/templates/com/news_edit.jinja +#: core/templates/core/user_tools.jinja msgid "Create news" msgstr "Créer nouvelle" -#: com/templates/com/news_admin_list.jinja:14 +#: com/templates/com/news_admin_list.jinja msgid "Notices" msgstr "Information" -#: com/templates/com/news_admin_list.jinja:16 +#: com/templates/com/news_admin_list.jinja msgid "Displayed notices" msgstr "Informations affichées" -#: com/templates/com/news_admin_list.jinja:20 -#: com/templates/com/news_admin_list.jinja:51 -#: com/templates/com/news_admin_list.jinja:84 -#: com/templates/com/news_admin_list.jinja:127 -#: com/templates/com/news_admin_list.jinja:172 -#: com/templates/com/news_admin_list.jinja:209 -#: com/templates/com/news_admin_list.jinja:248 -#: com/templates/com/news_admin_list.jinja:285 -#: launderette/templates/launderette/launderette_admin.jinja:42 -#: launderette/views.py:215 +#: com/templates/com/news_admin_list.jinja +#: launderette/templates/launderette/launderette_admin.jinja +#: launderette/views.py msgid "Type" msgstr "Type" -#: com/templates/com/news_admin_list.jinja:21 -#: com/templates/com/news_admin_list.jinja:52 -#: com/templates/com/news_admin_list.jinja:85 -#: com/templates/com/news_admin_list.jinja:128 -#: com/templates/com/news_admin_list.jinja:173 -#: com/templates/com/news_admin_list.jinja:210 -#: com/templates/com/news_admin_list.jinja:249 -#: com/templates/com/news_admin_list.jinja:286 -#: com/templates/com/weekmail.jinja:19 com/templates/com/weekmail.jinja:48 -#: forum/templates/forum/forum.jinja:32 forum/templates/forum/forum.jinja:51 -#: forum/templates/forum/main.jinja:34 forum/views.py:255 -#: pedagogy/templates/pedagogy/guide.jinja:92 +#: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja +#: forum/templates/forum/forum.jinja forum/templates/forum/main.jinja +#: forum/views.py pedagogy/templates/pedagogy/guide.jinja msgid "Title" msgstr "Titre" -#: com/templates/com/news_admin_list.jinja:22 -#: com/templates/com/news_admin_list.jinja:53 -#: com/templates/com/news_admin_list.jinja:86 -#: com/templates/com/news_admin_list.jinja:129 -#: com/templates/com/news_admin_list.jinja:174 -#: com/templates/com/news_admin_list.jinja:211 -#: com/templates/com/news_admin_list.jinja:250 -#: com/templates/com/news_admin_list.jinja:287 +#: com/templates/com/news_admin_list.jinja msgid "Summary" msgstr "Résumé" -#: com/templates/com/news_admin_list.jinja:24 -#: com/templates/com/news_admin_list.jinja:55 -#: com/templates/com/news_admin_list.jinja:88 -#: com/templates/com/news_admin_list.jinja:131 -#: com/templates/com/news_admin_list.jinja:176 -#: com/templates/com/news_admin_list.jinja:213 -#: com/templates/com/news_admin_list.jinja:252 -#: com/templates/com/news_admin_list.jinja:289 -#: com/templates/com/weekmail.jinja:17 com/templates/com/weekmail.jinja:46 -#: forum/templates/forum/forum.jinja:55 sas/models.py:297 +#: com/templates/com/news_admin_list.jinja com/templates/com/weekmail.jinja +#: forum/templates/forum/forum.jinja sas/models.py msgid "Author" msgstr "Auteur" -#: com/templates/com/news_admin_list.jinja:25 -#: com/templates/com/news_admin_list.jinja:89 -#: com/templates/com/news_admin_list.jinja:177 -#: com/templates/com/news_admin_list.jinja:253 +#: com/templates/com/news_admin_list.jinja msgid "Moderator" msgstr "Modérateur" -#: com/templates/com/news_admin_list.jinja:47 +#: com/templates/com/news_admin_list.jinja msgid "Notices to moderate" msgstr "Informations à modérer" -#: com/templates/com/news_admin_list.jinja:78 +#: com/templates/com/news_admin_list.jinja msgid "Weeklies" msgstr "Nouvelles hebdomadaires" -#: com/templates/com/news_admin_list.jinja:80 +#: com/templates/com/news_admin_list.jinja msgid "Displayed weeklies" msgstr "Nouvelles hebdomadaires affichées" -#: com/templates/com/news_admin_list.jinja:90 -#: com/templates/com/news_admin_list.jinja:132 -#: trombi/templates/trombi/edit_profile.jinja:24 +#: com/templates/com/news_admin_list.jinja +#: trombi/templates/trombi/edit_profile.jinja msgid "Dates" msgstr "Dates" -#: com/templates/com/news_admin_list.jinja:123 +#: com/templates/com/news_admin_list.jinja msgid "Weeklies to moderate" msgstr "Nouvelles hebdomadaires à modérer" -#: com/templates/com/news_admin_list.jinja:166 +#: com/templates/com/news_admin_list.jinja msgid "Calls" msgstr "Appels" -#: com/templates/com/news_admin_list.jinja:168 +#: com/templates/com/news_admin_list.jinja msgid "Displayed calls" msgstr "Appels affichés" -#: com/templates/com/news_admin_list.jinja:205 +#: com/templates/com/news_admin_list.jinja msgid "Calls to moderate" msgstr "Appels à modérer" -#: com/templates/com/news_admin_list.jinja:242 -#: core/templates/core/base/navbar.jinja:14 +#: com/templates/com/news_admin_list.jinja +#: core/templates/core/base/navbar.jinja msgid "Events" msgstr "Événements" -#: com/templates/com/news_admin_list.jinja:244 +#: com/templates/com/news_admin_list.jinja msgid "Displayed events" msgstr "Événements affichés" -#: com/templates/com/news_admin_list.jinja:281 +#: com/templates/com/news_admin_list.jinja msgid "Events to moderate" msgstr "Événements à modérer" -#: com/templates/com/news_detail.jinja:15 +#: com/templates/com/news_detail.jinja msgid "Back to news" msgstr "Retour aux nouvelles" -#: com/templates/com/news_detail.jinja:35 com/templates/com/news_edit.jinja:25 +#: com/templates/com/news_detail.jinja com/templates/com/news_edit.jinja msgid "Author: " msgstr "Auteur : " -#: com/templates/com/news_detail.jinja:37 +#: com/templates/com/news_detail.jinja msgid "Moderator: " msgstr "Modérateur : " -#: com/templates/com/news_detail.jinja:42 +#: com/templates/com/news_detail.jinja msgid "Edit (will be moderated again)" msgstr "Éditer (sera soumise de nouveau à la modération)" -#: com/templates/com/news_edit.jinja:6 com/templates/com/news_edit.jinja:29 +#: com/templates/com/news_edit.jinja msgid "Edit news" msgstr "Éditer la nouvelle" -#: com/templates/com/news_edit.jinja:41 +#: com/templates/com/news_edit.jinja msgid "Notice: Information, election result - no date" msgstr "Information, résultat d'élection - sans date" -#: com/templates/com/news_edit.jinja:42 +#: com/templates/com/news_edit.jinja msgid "Event: punctual event, associated with one date" msgstr "Événement : événement ponctuel associé à une date" -#: com/templates/com/news_edit.jinja:44 +#: com/templates/com/news_edit.jinja msgid "" "Weekly: recurrent event, associated with many dates (specify the first one, " "and a deadline)" @@ -1719,183 +1535,171 @@ msgstr "" "Hebdomadaire : événement récurrent, associé à plusieurs dates (spécifier la " "première, ainsi que la date de fin)" -#: com/templates/com/news_edit.jinja:50 +#: com/templates/com/news_edit.jinja msgid "" "Call: long time event, associated with a long date (like election appliance)" msgstr "" "Appel : événement de longue durée, associé à une longue date (comme des " "candidatures à une élection)" -#: com/templates/com/news_edit.jinja:102 com/templates/com/weekmail.jinja:10 +#: com/templates/com/news_edit.jinja com/templates/com/weekmail.jinja msgid "Preview" msgstr "Prévisualiser" -#: com/templates/com/news_list.jinja:11 +#: com/templates/com/news_list.jinja msgid "Administrate news" msgstr "Administrer les news" -#: com/templates/com/news_list.jinja:39 +#: com/templates/com/news_list.jinja msgid "Events today and the next few days" msgstr "Événements aujourd'hui et dans les prochains jours" -#: com/templates/com/news_list.jinja:82 +#: com/templates/com/news_list.jinja msgid "Nothing to come..." msgstr "Rien à venir..." -#: com/templates/com/news_list.jinja:89 +#: com/templates/com/news_list.jinja msgid "Coming soon... don't miss!" msgstr "Prochainement... à ne pas rater!" -#: com/templates/com/news_list.jinja:101 +#: com/templates/com/news_list.jinja msgid "All coming events" msgstr "Tous les événements à venir" -#: com/templates/com/news_list.jinja:113 +#: com/templates/com/news_list.jinja msgid "Agenda" msgstr "Agenda" -#: com/templates/com/news_list.jinja:137 +#: com/templates/com/news_list.jinja msgid "Birthdays" msgstr "Anniversaires" -#: com/templates/com/news_list.jinja:143 +#: com/templates/com/news_list.jinja #, python-format msgid "%(age)s year old" msgstr "%(age)s ans" -#: com/templates/com/news_list.jinja:153 com/tests.py:101 com/tests.py:111 +#: com/templates/com/news_list.jinja com/tests.py msgid "You need an up to date subscription to access this content" msgstr "Votre cotisation doit être à jour pour accéder à cette section" -#: com/templates/com/poster_edit.jinja:4 com/templates/com/poster_list.jinja:10 -#: core/templates/core/poster_list.jinja:10 +#: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja +#: core/templates/core/poster_list.jinja msgid "Poster" msgstr "Affiche" -#: com/templates/com/poster_edit.jinja:13 -#: com/templates/com/poster_edit.jinja:15 -#: com/templates/com/poster_moderate.jinja:13 -#: com/templates/com/screen_edit.jinja:12 +#: com/templates/com/poster_edit.jinja com/templates/com/poster_moderate.jinja +#: com/templates/com/screen_edit.jinja msgid "List" msgstr "Liste" -#: com/templates/com/poster_edit.jinja:18 +#: com/templates/com/poster_edit.jinja msgid "Posters - edit" msgstr "Affiche - modifier" -#: com/templates/com/poster_list.jinja:20 -#: com/templates/com/poster_list.jinja:23 -#: com/templates/com/screen_list.jinja:13 -#: core/templates/core/poster_list.jinja:19 sas/templates/sas/main.jinja:75 +#: com/templates/com/poster_list.jinja com/templates/com/screen_list.jinja +#: core/templates/core/poster_list.jinja sas/templates/sas/main.jinja msgid "Create" msgstr "Créer" -#: com/templates/com/poster_list.jinja:21 -#: core/templates/core/poster_list.jinja:21 +#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja msgid "Moderation" msgstr "Modération" -#: com/templates/com/poster_list.jinja:31 -#: core/templates/core/poster_list.jinja:29 +#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja msgid "No posters" msgstr "Aucune affiche" -#: com/templates/com/poster_moderate.jinja:15 +#: com/templates/com/poster_moderate.jinja msgid "Posters - moderation" msgstr "Affiches - modération" -#: com/templates/com/poster_moderate.jinja:21 +#: com/templates/com/poster_moderate.jinja msgid "No objects" msgstr "Aucun éléments" -#: com/templates/com/screen_edit.jinja:4 +#: com/templates/com/screen_edit.jinja msgid "Screen" msgstr "Écran" -#: com/templates/com/screen_edit.jinja:14 +#: com/templates/com/screen_edit.jinja msgid "Screen - edit" msgstr "Écran - modifier" -#: com/templates/com/screen_list.jinja:4 com/templates/com/screen_list.jinja:11 -#: core/templates/core/user_tools.jinja:138 +#: com/templates/com/screen_list.jinja core/templates/core/user_tools.jinja msgid "Screens" msgstr "Écrans" -#: com/templates/com/screen_list.jinja:20 +#: com/templates/com/screen_list.jinja msgid "No screens" msgstr "Pas d'écran" -#: com/templates/com/screen_list.jinja:27 -#: com/templates/com/screen_slideshow.jinja:4 +#: com/templates/com/screen_list.jinja com/templates/com/screen_slideshow.jinja msgid "Slideshow" msgstr "Diaporama" -#: com/templates/com/weekmail.jinja:5 com/templates/com/weekmail.jinja:9 -#: com/views.py:104 core/templates/core/user_tools.jinja:129 +#: com/templates/com/weekmail.jinja com/views.py +#: core/templates/core/user_tools.jinja msgid "Weekmail" msgstr "Weekmail" -#: com/templates/com/weekmail.jinja:11 -#: com/templates/com/weekmail_preview.jinja:34 +#: com/templates/com/weekmail.jinja com/templates/com/weekmail_preview.jinja msgid "Send" msgstr "Envoyer" -#: com/templates/com/weekmail.jinja:12 +#: com/templates/com/weekmail.jinja msgid "New article" msgstr "Nouvel article" -#: com/templates/com/weekmail.jinja:13 +#: com/templates/com/weekmail.jinja msgid "Articles in no weekmail yet" msgstr "Articles dans aucun weekmail" -#: com/templates/com/weekmail.jinja:20 com/templates/com/weekmail.jinja:49 -#: core/templates/core/macros.jinja:301 +#: com/templates/com/weekmail.jinja core/templates/core/macros.jinja msgid "Content" msgstr "Contenu" -#: com/templates/com/weekmail.jinja:34 +#: com/templates/com/weekmail.jinja msgid "Add to weekmail" msgstr "Ajouter au Weekmail" -#: com/templates/com/weekmail.jinja:35 com/templates/com/weekmail.jinja:64 +#: com/templates/com/weekmail.jinja msgid "Up" msgstr "Monter" -#: com/templates/com/weekmail.jinja:36 com/templates/com/weekmail.jinja:65 +#: com/templates/com/weekmail.jinja msgid "Down" msgstr "Descendre" -#: com/templates/com/weekmail.jinja:42 +#: com/templates/com/weekmail.jinja msgid "Articles included the next weekmail" msgstr "Article inclus dans le prochain Weekmail" -#: com/templates/com/weekmail.jinja:63 +#: com/templates/com/weekmail.jinja msgid "Delete from weekmail" msgstr "Supprimer du Weekmail" -#: com/templates/com/weekmail_preview.jinja:9 -#: core/templates/core/user_account_detail.jinja:10 -#: core/templates/core/user_account_detail.jinja:116 launderette/views.py:208 -#: pedagogy/templates/pedagogy/uv_detail.jinja:16 -#: pedagogy/templates/pedagogy/uv_detail.jinja:25 -#: trombi/templates/trombi/comment_moderation.jinja:10 -#: trombi/templates/trombi/export.jinja:9 +#: com/templates/com/weekmail_preview.jinja +#: core/templates/core/user_account_detail.jinja launderette/views.py +#: pedagogy/templates/pedagogy/uv_detail.jinja +#: trombi/templates/trombi/comment_moderation.jinja +#: trombi/templates/trombi/export.jinja msgid "Back" msgstr "Retour" -#: com/templates/com/weekmail_preview.jinja:13 +#: com/templates/com/weekmail_preview.jinja msgid "The following recipients were refused by the SMTP:" msgstr "Les destinataires suivants ont été refusé par le SMTP :" -#: com/templates/com/weekmail_preview.jinja:24 +#: com/templates/com/weekmail_preview.jinja msgid "Clean subscribers" msgstr "Nettoyer les abonnements" -#: com/templates/com/weekmail_preview.jinja:28 +#: com/templates/com/weekmail_preview.jinja msgid "Are you sure you want to send this weekmail?" msgstr "Êtes-vous sûr de vouloir envoyer ce Weekmail ?" -#: com/templates/com/weekmail_preview.jinja:30 +#: com/templates/com/weekmail_preview.jinja msgid "" "Warning: you are sending the weekmail in another language than the default " "one!" @@ -1903,134 +1707,133 @@ msgstr "" "Attention : vous allez envoyer le Weekmail dans un langage différent de " "celui par défaut !" -#: com/templates/com/weekmail_renderer_html.jinja:18 -#: com/templates/com/weekmail_renderer_text.jinja:4 +#: com/templates/com/weekmail_renderer_html.jinja +#: com/templates/com/weekmail_renderer_text.jinja msgid "Intro" msgstr "Intro" -#: com/templates/com/weekmail_renderer_html.jinja:22 -#: com/templates/com/weekmail_renderer_text.jinja:8 +#: com/templates/com/weekmail_renderer_html.jinja +#: com/templates/com/weekmail_renderer_text.jinja msgid "Table of content" msgstr "Sommaire" -#: com/templates/com/weekmail_renderer_html.jinja:35 -#: com/templates/com/weekmail_renderer_text.jinja:19 +#: com/templates/com/weekmail_renderer_html.jinja +#: com/templates/com/weekmail_renderer_text.jinja msgid "Joke" msgstr "Blague" -#: com/templates/com/weekmail_renderer_html.jinja:40 -#: com/templates/com/weekmail_renderer_text.jinja:24 +#: com/templates/com/weekmail_renderer_html.jinja +#: com/templates/com/weekmail_renderer_text.jinja msgid "Pro tip" msgstr "Astuce" -#: com/templates/com/weekmail_renderer_html.jinja:45 -#: com/templates/com/weekmail_renderer_text.jinja:29 +#: com/templates/com/weekmail_renderer_html.jinja +#: com/templates/com/weekmail_renderer_text.jinja msgid "Final word" msgstr "Le mot de la fin" -#: com/views.py:75 +#: com/views.py msgid "Format: 16:9 | Resolution: 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080" -#: com/views.py:78 com/views.py:199 election/views.py:167 -#: subscription/forms.py:18 +#: com/views.py election/views.py subscription/forms.py msgid "Start date" msgstr "Date de début" -#: com/views.py:99 +#: com/views.py msgid "Communication administration" msgstr "Administration de la communication" -#: com/views.py:110 core/templates/core/user_tools.jinja:130 +#: com/views.py core/templates/core/user_tools.jinja msgid "Weekmail destinations" msgstr "Destinataires du Weekmail" -#: com/views.py:114 +#: com/views.py msgid "Info message" msgstr "Message d'info" -#: com/views.py:120 +#: com/views.py msgid "Alert message" msgstr "Message d'alerte" -#: com/views.py:141 +#: com/views.py msgid "Screens list" msgstr "Liste d'écrans" -#: com/views.py:204 +#: com/views.py msgid "Until" msgstr "Jusqu'à" -#: com/views.py:206 +#: com/views.py msgid "Automoderation" msgstr "Automodération" -#: com/views.py:213 com/views.py:217 com/views.py:229 +#: com/views.py msgid "This field is required." msgstr "Ce champ est obligatoire." -#: com/views.py:226 +#: com/views.py msgid "An event cannot end before its beginning." msgstr "Un évènement ne peut pas se finir avant d'avoir commencé." -#: com/views.py:446 +#: com/views.py msgid "Delete and save to regenerate" msgstr "Supprimer et sauver pour régénérer" -#: com/views.py:461 +#: com/views.py msgid "Weekmail of the " msgstr "Weekmail du " -#: com/views.py:565 +#: com/views.py msgid "" "You must be a board member of the selected club to post in the Weekmail." msgstr "" "Vous devez êtres un membre du bureau du club sélectionné pour poster dans le " "Weekmail." -#: core/models.py:64 +#: core/models.py msgid "Is manually manageable" msgstr "Est gérable manuellement" -#: core/models.py:66 +#: core/models.py msgid "If False, this shouldn't be shown on group management pages" msgstr "" "Si faux, ceci ne devrait pas être montré sur les pages de gestion des groupes" -#: core/models.py:90 +#: core/models.py #, python-format msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" -#: core/models.py:173 +#: core/models.py msgid "first name" msgstr "Prénom" -#: core/models.py:174 +#: core/models.py msgid "last name" msgstr "Nom" -#: core/models.py:175 +#: core/models.py msgid "email address" msgstr "adresse email" -#: core/models.py:176 +#: core/models.py msgid "date of birth" msgstr "date de naissance" -#: core/models.py:177 +#: core/models.py msgid "nick name" msgstr "surnom" -#: core/models.py:178 +#: core/models.py msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:181 +#: core/models.py msgid "groups" msgstr "groupes" -#: core/models.py:183 +#: core/models.py msgid "" "The groups this user belongs to. A user will get all permissions granted to " "each of their groups." @@ -2038,261 +1841,248 @@ msgstr "" "Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes " "les permissions de chacun de ses groupes." -#: core/models.py:200 +#: core/models.py msgid "profile" msgstr "profil" -#: core/models.py:208 +#: core/models.py msgid "avatar" msgstr "avatar" -#: core/models.py:216 +#: core/models.py msgid "scrub" msgstr "blouse" -#: core/models.py:222 +#: core/models.py msgid "sex" msgstr "Genre" -#: core/models.py:226 +#: core/models.py msgid "Man" msgstr "Homme" -#: core/models.py:226 +#: core/models.py msgid "Woman" msgstr "Femme" -#: core/models.py:228 +#: core/models.py msgid "pronouns" msgstr "pronoms" -#: core/models.py:230 +#: core/models.py msgid "tshirt size" msgstr "taille de t-shirt" -#: core/models.py:233 +#: core/models.py msgid "-" msgstr "-" -#: core/models.py:234 +#: core/models.py msgid "XS" msgstr "XS" -#: core/models.py:235 +#: core/models.py msgid "S" msgstr "S" -#: core/models.py:236 +#: core/models.py msgid "M" msgstr "M" -#: core/models.py:237 +#: core/models.py msgid "L" msgstr "L" -#: core/models.py:238 +#: core/models.py msgid "XL" msgstr "XL" -#: core/models.py:239 +#: core/models.py msgid "XXL" msgstr "XXL" -#: core/models.py:240 +#: core/models.py msgid "XXXL" msgstr "XXXL" -#: core/models.py:248 +#: core/models.py msgid "Student" msgstr "Étudiant" -#: core/models.py:249 +#: core/models.py msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:250 +#: core/models.py msgid "Teacher" msgstr "Enseignant" -#: core/models.py:251 +#: core/models.py msgid "Agent" msgstr "Personnel" -#: core/models.py:252 +#: core/models.py msgid "Doctor" msgstr "Doctorant" -#: core/models.py:253 +#: core/models.py msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:254 +#: core/models.py msgid "Service" msgstr "Service" -#: core/models.py:260 +#: core/models.py msgid "department" msgstr "département" -#: core/models.py:267 +#: core/models.py msgid "dpt option" msgstr "Filière" -#: core/models.py:269 pedagogy/models.py:70 pedagogy/models.py:294 +#: core/models.py pedagogy/models.py msgid "semester" msgstr "semestre" -#: core/models.py:270 +#: core/models.py msgid "quote" msgstr "citation" -#: core/models.py:271 +#: core/models.py msgid "school" msgstr "école" -#: core/models.py:273 +#: core/models.py msgid "promo" msgstr "promo" -#: core/models.py:276 +#: core/models.py msgid "forum signature" msgstr "signature du forum" -#: core/models.py:278 +#: core/models.py msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:280 +#: core/models.py msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:283 +#: core/models.py msgid "parent address" msgstr "adresse des parents" -#: core/models.py:286 +#: core/models.py msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:466 +#: core/models.py msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:620 core/templates/core/macros.jinja:80 -#: core/templates/core/macros.jinja:84 core/templates/core/macros.jinja:85 -#: core/templates/core/user_detail.jinja:100 -#: core/templates/core/user_detail.jinja:101 -#: core/templates/core/user_detail.jinja:103 -#: core/templates/core/user_detail.jinja:104 -#: core/templates/core/user_detail.jinja:109 -#: core/templates/core/user_detail.jinja:110 -#: core/templates/core/user_detail.jinja:112 -#: core/templates/core/user_detail.jinja:113 -#: core/templates/core/user_edit.jinja:21 -#: election/templates/election/election_detail.jinja:136 -#: election/templates/election/election_detail.jinja:138 -#: forum/templates/forum/macros.jinja:105 -#: forum/templates/forum/macros.jinja:107 -#: forum/templates/forum/macros.jinja:109 -#: trombi/templates/trombi/user_tools.jinja:42 +#: core/models.py core/templates/core/macros.jinja +#: core/templates/core/user_detail.jinja core/templates/core/user_edit.jinja +#: election/templates/election/election_detail.jinja +#: forum/templates/forum/macros.jinja trombi/templates/trombi/user_tools.jinja msgid "Profile" msgstr "Profil" -#: core/models.py:731 +#: core/models.py msgid "Visitor" msgstr "Visiteur" -#: core/models.py:738 +#: core/models.py msgid "receive the Weekmail" msgstr "recevoir le Weekmail" -#: core/models.py:739 +#: core/models.py msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:741 +#: core/models.py msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:744 +#: core/models.py msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:770 sas/forms.py:81 +#: core/models.py sas/forms.py msgid "file name" msgstr "nom du fichier" -#: core/models.py:774 core/models.py:1125 +#: core/models.py msgid "parent" msgstr "parent" -#: core/models.py:788 +#: core/models.py msgid "compressed file" msgstr "version allégée" -#: core/models.py:795 +#: core/models.py msgid "thumbnail" msgstr "miniature" -#: core/models.py:803 core/models.py:820 +#: core/models.py msgid "owner" msgstr "propriétaire" -#: core/models.py:807 core/models.py:1142 +#: core/models.py msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:810 core/models.py:1145 +#: core/models.py msgid "view group" msgstr "groupe de vue" -#: core/models.py:812 +#: core/models.py msgid "is folder" msgstr "est un dossier" -#: core/models.py:813 +#: core/models.py msgid "mime type" msgstr "type mime" -#: core/models.py:814 +#: core/models.py msgid "size" msgstr "taille" -#: core/models.py:825 +#: core/models.py msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:827 +#: core/models.py msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:894 +#: core/models.py msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:896 core/models.py:900 +#: core/models.py msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:903 +#: core/models.py msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:914 +#: core/models.py msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:931 +#: core/models.py msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1108 +#: core/models.py msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1114 +#: core/models.py msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2300,403 +2090,380 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1132 +#: core/models.py msgid "page name" msgstr "nom de la page" -#: core/models.py:1137 +#: core/models.py msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1150 +#: core/models.py msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1157 +#: core/models.py msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1207 +#: core/models.py msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1210 +#: core/models.py msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1321 +#: core/models.py msgid "revision" msgstr "révision" -#: core/models.py:1322 +#: core/models.py msgid "page title" msgstr "titre de la page" -#: core/models.py:1323 +#: core/models.py msgid "page content" msgstr "contenu de la page" -#: core/models.py:1364 +#: core/models.py msgid "url" msgstr "url" -#: core/models.py:1365 +#: core/models.py msgid "param" msgstr "param" -#: core/models.py:1370 +#: core/models.py msgid "viewed" msgstr "vue" -#: core/models.py:1428 +#: core/models.py msgid "operation type" msgstr "type d'opération" -#: core/templates/core/403.jinja:5 +#: core/templates/core/403.jinja msgid "403, Forbidden" msgstr "403, Non autorisé" -#: core/templates/core/404.jinja:6 +#: core/templates/core/404.jinja msgid "404, Not Found" msgstr "404. Non trouvé" -#: core/templates/core/500.jinja:9 +#: core/templates/core/500.jinja msgid "500, Server Error" msgstr "500, Erreur Serveur" -#: core/templates/core/base.jinja:5 +#: core/templates/core/base.jinja msgid "Welcome!" msgstr "Bienvenue !" -#: core/templates/core/base.jinja:105 core/templates/core/base/navbar.jinja:43 +#: core/templates/core/base.jinja core/templates/core/base/navbar.jinja msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:106 +#: core/templates/core/base.jinja msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:107 +#: core/templates/core/base.jinja msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:108 +#: core/templates/core/base.jinja msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:109 +#: core/templates/core/base.jinja msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:112 +#: core/templates/core/base.jinja msgid "Site created by the IT Department of the AE" msgstr "Site réalisé par le Pôle Informatique de l'AE" -#: core/templates/core/base/header.jinja:13 core/templates/core/login.jinja:8 -#: core/templates/core/login.jinja:18 core/templates/core/login.jinja:50 -#: core/templates/core/password_reset_complete.jinja:5 +#: core/templates/core/base/header.jinja core/templates/core/login.jinja +#: core/templates/core/password_reset_complete.jinja msgid "Login" msgstr "Connexion" -#: core/templates/core/base/header.jinja:14 -#: core/templates/core/register.jinja:7 core/templates/core/register.jinja:16 -#: core/templates/core/register.jinja:22 +#: core/templates/core/base/header.jinja core/templates/core/register.jinja msgid "Register" msgstr "Inscription" -#: core/templates/core/base/header.jinja:20 -#: core/templates/core/base/header.jinja:21 -#: forum/templates/forum/macros.jinja:179 -#: forum/templates/forum/macros.jinja:183 -#: matmat/templates/matmat/search_form.jinja:39 -#: matmat/templates/matmat/search_form.jinja:48 -#: matmat/templates/matmat/search_form.jinja:58 +#: core/templates/core/base/header.jinja forum/templates/forum/macros.jinja +#: matmat/templates/matmat/search_form.jinja msgid "Search" msgstr "Recherche" -#: core/templates/core/base/header.jinja:62 +#: core/templates/core/base/header.jinja msgid "Logout" msgstr "Déconnexion" -#: core/templates/core/base/header.jinja:110 +#: core/templates/core/base/header.jinja msgid "You do not have any unread notification" msgstr "Vous n'avez aucune notification non lue" -#: core/templates/core/base/header.jinja:115 +#: core/templates/core/base/header.jinja msgid "View more" msgstr "Voir plus" -#: core/templates/core/base/header.jinja:118 -#: forum/templates/forum/last_unread.jinja:21 +#: core/templates/core/base/header.jinja +#: forum/templates/forum/last_unread.jinja msgid "Mark all as read" msgstr "Marquer tout comme lu" -#: core/templates/core/base/navbar.jinja:4 +#: core/templates/core/base/navbar.jinja msgid "Main" msgstr "Accueil" -#: core/templates/core/base/navbar.jinja:6 +#: core/templates/core/base/navbar.jinja msgid "Associations & Clubs" msgstr "Associations & Clubs" -#: core/templates/core/base/navbar.jinja:8 +#: core/templates/core/base/navbar.jinja msgid "AE" msgstr "L'AE" -#: core/templates/core/base/navbar.jinja:9 +#: core/templates/core/base/navbar.jinja msgid "AE's clubs" msgstr "Les clubs de L'AE" -#: core/templates/core/base/navbar.jinja:10 +#: core/templates/core/base/navbar.jinja msgid "Others UTBM's Associations" msgstr "Les autres associations de l'UTBM" -#: core/templates/core/base/navbar.jinja:16 -#: core/templates/core/user_tools.jinja:172 +#: core/templates/core/base/navbar.jinja core/templates/core/user_tools.jinja msgid "Elections" msgstr "Élections" -#: core/templates/core/base/navbar.jinja:17 +#: core/templates/core/base/navbar.jinja msgid "Big event" msgstr "Grandes Activités" -#: core/templates/core/base/navbar.jinja:20 -#: forum/templates/forum/favorite_topics.jinja:18 -#: forum/templates/forum/last_unread.jinja:18 -#: forum/templates/forum/macros.jinja:90 forum/templates/forum/main.jinja:6 -#: forum/templates/forum/main.jinja:15 forum/templates/forum/main.jinja:18 -#: forum/templates/forum/reply.jinja:21 +#: core/templates/core/base/navbar.jinja +#: forum/templates/forum/favorite_topics.jinja +#: forum/templates/forum/last_unread.jinja forum/templates/forum/macros.jinja +#: forum/templates/forum/main.jinja forum/templates/forum/reply.jinja msgid "Forum" msgstr "Forum" -#: core/templates/core/base/navbar.jinja:21 +#: core/templates/core/base/navbar.jinja msgid "Gallery" msgstr "Photos" -#: core/templates/core/base/navbar.jinja:22 counter/models.py:493 -#: counter/templates/counter/counter_list.jinja:11 -#: eboutic/templates/eboutic/eboutic_main.jinja:4 -#: eboutic/templates/eboutic/eboutic_main.jinja:23 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:16 -#: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:423 sith/settings.py:431 +#: core/templates/core/base/navbar.jinja counter/models.py +#: counter/templates/counter/counter_list.jinja +#: eboutic/templates/eboutic/eboutic_main.jinja +#: eboutic/templates/eboutic/eboutic_makecommand.jinja +#: eboutic/templates/eboutic/eboutic_payment_result.jinja sith/settings.py msgid "Eboutic" msgstr "Eboutic" -#: core/templates/core/base/navbar.jinja:24 +#: core/templates/core/base/navbar.jinja msgid "Services" msgstr "Services" -#: core/templates/core/base/navbar.jinja:26 +#: core/templates/core/base/navbar.jinja msgid "Matmatronch" msgstr "Matmatronch" -#: core/templates/core/base/navbar.jinja:27 launderette/models.py:38 -#: launderette/templates/launderette/launderette_book.jinja:5 -#: launderette/templates/launderette/launderette_book_choose.jinja:4 -#: launderette/templates/launderette/launderette_main.jinja:4 +#: core/templates/core/base/navbar.jinja launderette/models.py +#: launderette/templates/launderette/launderette_book.jinja +#: launderette/templates/launderette/launderette_book_choose.jinja +#: launderette/templates/launderette/launderette_main.jinja msgid "Launderette" msgstr "Laverie" -#: core/templates/core/base/navbar.jinja:28 core/templates/core/file.jinja:24 -#: core/views/files.py:122 +#: core/templates/core/base/navbar.jinja core/templates/core/file.jinja +#: core/views/files.py msgid "Files" msgstr "Fichiers" -#: core/templates/core/base/navbar.jinja:29 -#: core/templates/core/user_tools.jinja:163 +#: core/templates/core/base/navbar.jinja core/templates/core/user_tools.jinja msgid "Pedagogy" msgstr "Pédagogie" -#: core/templates/core/base/navbar.jinja:33 +#: core/templates/core/base/navbar.jinja msgid "My Benefits" msgstr "Mes Avantages" -#: core/templates/core/base/navbar.jinja:35 +#: core/templates/core/base/navbar.jinja msgid "Sponsors" msgstr "Partenaires" -#: core/templates/core/base/navbar.jinja:36 +#: core/templates/core/base/navbar.jinja msgid "Subscriber benefits" msgstr "Les avantages cotisants" -#: core/templates/core/base/navbar.jinja:40 +#: core/templates/core/base/navbar.jinja msgid "Help" msgstr "Aide" -#: core/templates/core/base/navbar.jinja:42 +#: core/templates/core/base/navbar.jinja msgid "FAQ" msgstr "FAQ" -#: core/templates/core/base/navbar.jinja:44 +#: core/templates/core/base/navbar.jinja msgid "Wiki" msgstr "Wiki" -#: core/templates/core/create.jinja:4 core/templates/core/create.jinja:8 +#: core/templates/core/create.jinja #, python-format msgid "Create %(name)s" msgstr "Créer %(name)s" -#: core/templates/core/delete_confirm.jinja:4 -#: core/templates/core/delete_confirm.jinja:14 -#: core/templates/core/file_delete_confirm.jinja:4 -#: core/templates/core/file_delete_confirm.jinja:18 +#: core/templates/core/delete_confirm.jinja +#: core/templates/core/file_delete_confirm.jinja msgid "Delete confirmation" msgstr "Confirmation de suppression" -#: core/templates/core/delete_confirm.jinja:16 -#: core/templates/core/file_delete_confirm.jinja:29 -#: counter/templates/counter/fragments/delete_student_card.jinja:4 +#: core/templates/core/delete_confirm.jinja +#: core/templates/core/file_delete_confirm.jinja +#: counter/templates/counter/fragments/delete_student_card.jinja #, python-format msgid "Are you sure you want to delete \"%(obj)s\"?" msgstr "Êtes-vous sûr de vouloir supprimer \"%(obj)s\" ?" -#: core/templates/core/delete_confirm.jinja:17 -#: core/templates/core/file_delete_confirm.jinja:36 -#: counter/templates/counter/fragments/delete_student_card.jinja:5 +#: core/templates/core/delete_confirm.jinja +#: core/templates/core/file_delete_confirm.jinja +#: counter/templates/counter/fragments/delete_student_card.jinja msgid "Confirm" msgstr "Confirmation" -#: core/templates/core/delete_confirm.jinja:20 -#: core/templates/core/file_delete_confirm.jinja:46 -#: counter/templates/counter/counter_click.jinja:155 -#: counter/templates/counter/fragments/delete_student_card.jinja:12 -#: sas/templates/sas/ask_picture_removal.jinja:20 +#: core/templates/core/delete_confirm.jinja +#: core/templates/core/file_delete_confirm.jinja +#: counter/templates/counter/counter_click.jinja +#: counter/templates/counter/fragments/delete_student_card.jinja +#: sas/templates/sas/ask_picture_removal.jinja msgid "Cancel" msgstr "Annuler" -#: core/templates/core/edit.jinja:5 core/templates/core/edit.jinja:13 -#: core/templates/core/file_edit.jinja:4 -#: counter/templates/counter/cash_register_summary.jinja:4 +#: core/templates/core/edit.jinja core/templates/core/file_edit.jinja +#: counter/templates/counter/cash_register_summary.jinja #, python-format msgid "Edit %(obj)s" msgstr "Éditer %(obj)s" -#: core/templates/core/file.jinja:11 core/templates/core/file_list.jinja:6 +#: core/templates/core/file.jinja core/templates/core/file_list.jinja msgid "File list" msgstr "Liste de fichiers" -#: core/templates/core/file.jinja:13 +#: core/templates/core/file.jinja msgid "New file" msgstr "Nouveau fichier" -#: core/templates/core/file.jinja:15 core/templates/core/page.jinja:11 +#: core/templates/core/file.jinja core/templates/core/page.jinja msgid "Not found" msgstr "Non trouvé" -#: core/templates/core/file.jinja:36 +#: core/templates/core/file.jinja msgid "My files" msgstr "Mes fichiers" -#: core/templates/core/file.jinja:45 core/templates/core/page.jinja:38 +#: core/templates/core/file.jinja core/templates/core/page.jinja msgid "Prop" msgstr "Propriétés" -#: core/templates/core/file_detail.jinja:13 -#: core/templates/core/file_moderation.jinja:35 -#: sas/templates/sas/picture.jinja:103 +#: core/templates/core/file_detail.jinja +#: core/templates/core/file_moderation.jinja sas/templates/sas/picture.jinja msgid "Owner: " msgstr "Propriétaire : " -#: core/templates/core/file_detail.jinja:26 sas/templates/sas/album.jinja:50 -#: sas/templates/sas/main.jinja:49 +#: core/templates/core/file_detail.jinja sas/templates/sas/album.jinja +#: sas/templates/sas/main.jinja msgid "Clear clipboard" msgstr "Vider le presse-papier" -#: core/templates/core/file_detail.jinja:27 sas/templates/sas/album.jinja:37 +#: core/templates/core/file_detail.jinja sas/templates/sas/album.jinja msgid "Cut" msgstr "Couper" -#: core/templates/core/file_detail.jinja:28 sas/templates/sas/album.jinja:38 +#: core/templates/core/file_detail.jinja sas/templates/sas/album.jinja msgid "Paste" msgstr "Coller" -#: core/templates/core/file_detail.jinja:31 sas/templates/sas/album.jinja:44 -#: sas/templates/sas/main.jinja:43 +#: core/templates/core/file_detail.jinja sas/templates/sas/album.jinja +#: sas/templates/sas/main.jinja msgid "Clipboard: " msgstr "Presse-papier : " -#: core/templates/core/file_detail.jinja:53 +#: core/templates/core/file_detail.jinja msgid "Real name: " msgstr "Nom réel : " -#: core/templates/core/file_detail.jinja:54 -#: core/templates/core/file_moderation.jinja:36 -#: sas/templates/sas/picture.jinja:94 +#: core/templates/core/file_detail.jinja +#: core/templates/core/file_moderation.jinja sas/templates/sas/picture.jinja msgid "Date: " msgstr "Date : " -#: core/templates/core/file_detail.jinja:56 +#: core/templates/core/file_detail.jinja msgid "Type: " msgstr "Type : " -#: core/templates/core/file_detail.jinja:57 +#: core/templates/core/file_detail.jinja msgid "Size: " msgstr "Taille : " -#: core/templates/core/file_detail.jinja:57 +#: core/templates/core/file_detail.jinja msgid "bytes" msgstr "octets" -#: core/templates/core/file_detail.jinja:59 +#: core/templates/core/file_detail.jinja msgid "Download" msgstr "Télécharger" -#: core/templates/core/file_list.jinja:19 +#: core/templates/core/file_list.jinja msgid "There is no file in this website." msgstr "Il n'y a pas de fichier sur ce site web." -#: core/templates/core/file_moderation.jinja:16 -#: core/templates/core/file_moderation.jinja:20 +#: core/templates/core/file_moderation.jinja msgid "File moderation" msgstr "Modération des fichiers" -#: core/templates/core/file_moderation.jinja:34 +#: core/templates/core/file_moderation.jinja msgid "Full name: " msgstr "Nom complet : " -#: core/templates/core/group_detail.jinja:5 +#: core/templates/core/group_detail.jinja msgid "Group detail" msgstr "Détail du groupe" -#: core/templates/core/group_detail.jinja:10 -#: core/templates/core/group_edit.jinja:4 +#: core/templates/core/group_detail.jinja core/templates/core/group_edit.jinja msgid "Back to list" msgstr "Retour à la liste" -#: core/templates/core/group_detail.jinja:12 +#: core/templates/core/group_detail.jinja msgid "No user in this group" msgstr "Aucun utilisateur dans ce groupe" -#: core/templates/core/group_edit.jinja:5 +#: core/templates/core/group_edit.jinja msgid "Edit group" msgstr "Éditer le groupe" -#: core/templates/core/group_edit.jinja:9 -#: core/templates/core/user_edit.jinja:167 -#: core/templates/core/user_group.jinja:13 -#: pedagogy/templates/pedagogy/uv_edit.jinja:36 +#: core/templates/core/group_edit.jinja core/templates/core/user_edit.jinja +#: core/templates/core/user_group.jinja +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "Update" msgstr "Mettre à jour" -#: core/templates/core/group_list.jinja:4 -#: core/templates/core/group_list.jinja:8 +#: core/templates/core/group_list.jinja msgid "Group list" msgstr "Liste des groupes" -#: core/templates/core/group_list.jinja:9 +#: core/templates/core/group_list.jinja msgid "New group" msgstr "Nouveau groupe" -#: core/templates/core/group_list.jinja:13 +#: core/templates/core/group_list.jinja msgid "ID" msgstr "ID" -#: core/templates/core/group_list.jinja:14 +#: core/templates/core/group_list.jinja msgid "Group" msgstr "Groupe" -#: core/templates/core/login.jinja:22 +#: core/templates/core/login.jinja msgid "" "Your account doesn't have access to this page. To proceed,\n" " please login with an account that has access." @@ -2704,72 +2471,72 @@ msgstr "" "Votre compte n'a pas accès à cette page. Merci de vous identifier avec un " "compte qui a accès." -#: core/templates/core/login.jinja:25 +#: core/templates/core/login.jinja msgid "Please login or create an account to see this page." msgstr "Merci de vous identifier ou de créer un compte pour voir cette page." -#: core/templates/core/login.jinja:31 +#: core/templates/core/login.jinja msgid "Your username and password didn't match. Please try again." msgstr "" "Votre nom d'utilisateur et votre mot de passe ne correspondent pas. Merci de " "réessayer." -#: core/templates/core/login.jinja:55 +#: core/templates/core/login.jinja msgid "Lost password?" msgstr "Mot de passe perdu ?" -#: core/templates/core/login.jinja:57 +#: core/templates/core/login.jinja msgid "Create account" msgstr "Créer un compte" -#: core/templates/core/macros.jinja:35 +#: core/templates/core/macros.jinja msgid "Share on Facebook" msgstr "Partager sur Facebook" -#: core/templates/core/macros.jinja:39 +#: core/templates/core/macros.jinja msgid "Tweet" msgstr "Tweeter" -#: core/templates/core/macros.jinja:93 +#: core/templates/core/macros.jinja #, python-format msgid "Subscribed until %(subscription_end)s" msgstr "Cotisant jusqu'au %(subscription_end)s" -#: core/templates/core/macros.jinja:94 +#: core/templates/core/macros.jinja msgid "Account number: " msgstr "Numéro de compte : " -#: core/templates/core/macros.jinja:99 launderette/models.py:188 +#: core/templates/core/macros.jinja launderette/models.py msgid "Slot" msgstr "Créneau" -#: core/templates/core/macros.jinja:112 -#: launderette/templates/launderette/launderette_admin.jinja:20 +#: core/templates/core/macros.jinja +#: launderette/templates/launderette/launderette_admin.jinja msgid "Tokens" msgstr "Jetons" -#: core/templates/core/macros.jinja:266 +#: core/templates/core/macros.jinja msgid "Select All" msgstr "Tout sélectionner" -#: core/templates/core/macros.jinja:267 +#: core/templates/core/macros.jinja msgid "Unselect All" msgstr "Tout désélectionner" -#: core/templates/core/macros_pages.jinja:4 +#: core/templates/core/macros_pages.jinja #, python-format msgid "You're seeing the history of page \"%(page_name)s\"" msgstr "Vous consultez l'historique de la page \"%(page_name)s\"" -#: core/templates/core/macros_pages.jinja:10 +#: core/templates/core/macros_pages.jinja msgid "last" msgstr "actuel" -#: core/templates/core/macros_pages.jinja:22 +#: core/templates/core/macros_pages.jinja msgid "Edit page" msgstr "Éditer la page" -#: core/templates/core/new_user_email.jinja:2 +#: core/templates/core/new_user_email.jinja msgid "" "You're receiving this email because you subscribed to the UTBM student " "association." @@ -2777,19 +2544,19 @@ msgstr "" "Vous avez reçu cet email parce que vous avez cotisé à l'Association des " "Étudiants de l'UTBM." -#: core/templates/core/new_user_email.jinja:4 -#: core/templates/core/password_reset_email.jinja:4 +#: core/templates/core/new_user_email.jinja +#: core/templates/core/password_reset_email.jinja msgid "Please go to the following page and choose a new password:" msgstr "" "Merci de vous rendre sur la page suivante et de choisir un nouveau mot de " "passe :" -#: core/templates/core/new_user_email.jinja:8 -#: core/templates/core/register_confirm_mail.jinja:4 +#: core/templates/core/new_user_email.jinja +#: core/templates/core/register_confirm_mail.jinja msgid "Your username, in case it was not given to you: " msgstr "Votre nom d'utilisateur, si il ne vous a pas été donné :" -#: core/templates/core/new_user_email.jinja:9 +#: core/templates/core/new_user_email.jinja msgid "" "You also got a new account that will be useful to purchase products in the " "living areas and on the Eboutic." @@ -2797,93 +2564,91 @@ msgstr "" "Un compte vous a également été créé, qui vous servira notamment à consommer " "dans les lieux de vie ou sur l'Eboutic." -#: core/templates/core/new_user_email.jinja:10 +#: core/templates/core/new_user_email.jinja #, python-format msgid "Here is your account number: %(account)s" msgstr "Voici votre numéro de compte AE : %(account)s" -#: core/templates/core/new_user_email.jinja:12 +#: core/templates/core/new_user_email.jinja msgid "Thanks for subscribing! " msgstr "Merci d'avoir cotisé !" -#: core/templates/core/new_user_email.jinja:14 -#: core/templates/core/register_confirm_mail.jinja:14 +#: core/templates/core/new_user_email.jinja +#: core/templates/core/register_confirm_mail.jinja msgid "The AE team" msgstr "L'équipe AE" -#: core/templates/core/new_user_email_subject.jinja:2 +#: core/templates/core/new_user_email_subject.jinja msgid "New subscription to the UTBM student association" msgstr "Nouvelle cotisation à l'Association des Étudiants de l'UTBM" -#: core/templates/core/notification_list.jinja:4 -#: core/templates/core/notification_list.jinja:8 +#: core/templates/core/notification_list.jinja msgid "Notification list" msgstr "Liste des notifications" -#: core/templates/core/page.jinja:7 core/templates/core/page_list.jinja:4 -#: core/templates/core/page_list.jinja:9 +#: core/templates/core/page.jinja core/templates/core/page_list.jinja msgid "Page list" msgstr "Liste des pages" -#: core/templates/core/page.jinja:9 +#: core/templates/core/page.jinja msgid "Create page" msgstr "Créer une page" -#: core/templates/core/page.jinja:29 +#: core/templates/core/page.jinja msgid "Return to club management" msgstr "Retourner à la gestion du club" -#: core/templates/core/page.jinja:49 +#: core/templates/core/page.jinja msgid "Page does not exist" msgstr "La page n'existe pas" -#: core/templates/core/page.jinja:51 +#: core/templates/core/page.jinja msgid "Create it?" msgstr "La créer ?" -#: core/templates/core/page_detail.jinja:5 +#: core/templates/core/page_detail.jinja #, python-format msgid "This may not be the last update, you are seeing revision %(rev_id)s!" msgstr "" "Ceci n'est peut-être pas la dernière version de la page. Vous consultez la " "version %(rev_id)s." -#: core/templates/core/page_hist.jinja:6 +#: core/templates/core/page_hist.jinja msgid "Page history" msgstr "Historique de la page" -#: core/templates/core/page_list.jinja:16 +#: 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:7 +#: core/templates/core/page_prop.jinja msgid "Page properties" msgstr "Propriétés de la page" -#: core/templates/core/password_change.jinja:6 +#: core/templates/core/password_change.jinja #, python-format msgid "Change password for %(user)s" msgstr "Changer le mot de passe de %(user)s" -#: core/templates/core/password_change.jinja:11 +#: core/templates/core/password_change.jinja msgid "Change" msgstr "Changer" -#: core/templates/core/password_change_done.jinja:4 +#: core/templates/core/password_change_done.jinja msgid "You successfully changed your password!" msgstr "Vous avez correctement changé votre mot de passe !" -#: core/templates/core/password_reset.jinja:8 -#: core/templates/core/password_reset_confirm.jinja:8 -#: core/templates/core/user_godfathers_tree.jinja:81 +#: core/templates/core/password_reset.jinja +#: core/templates/core/password_reset_confirm.jinja +#: core/templates/core/user_godfathers_tree.jinja msgid "Reset" msgstr "Réinitialiser" -#: core/templates/core/password_reset_complete.jinja:4 +#: core/templates/core/password_reset_complete.jinja msgid "You successfully reset your password!" msgstr "Vous avez correctement réinitialisé votre mot de passe !" -#: core/templates/core/password_reset_confirm.jinja:11 +#: core/templates/core/password_reset_confirm.jinja msgid "" "It seems that this link has expired. To generate a new link, you can follow " "this link: " @@ -2891,15 +2656,15 @@ msgstr "" "Il semble que le lien ai expiré. Pour générer un nouveau lien, tu peux " "suivre ce lien : " -#: core/templates/core/password_reset_confirm.jinja:11 +#: core/templates/core/password_reset_confirm.jinja msgid "lost password" msgstr "mot de passe perdu" -#: core/templates/core/password_reset_done.jinja:4 +#: core/templates/core/password_reset_done.jinja msgid "Password reset sent" msgstr "Réinitialisation de mot de passe envoyée" -#: core/templates/core/password_reset_done.jinja:7 +#: core/templates/core/password_reset_done.jinja msgid "" "We've emailed you instructions for setting your password, if an account " "exists with the email you entered. You should\n" @@ -2909,7 +2674,7 @@ msgstr "" "passe par email, si un compte avec l'email entré existe effectivement.\n" "Vous devriez les recevoir rapidement." -#: core/templates/core/password_reset_done.jinja:12 +#: core/templates/core/password_reset_done.jinja msgid "" "If you don't receive an email, please make sure you've entered the address " "you registered with, and check your spam\n" @@ -2919,7 +2684,7 @@ msgstr "" "l'adresse email avec laquelle vous vous êtes inscrit, et vérifiez votre " "dossier de spam." -#: core/templates/core/password_reset_email.jinja:2 +#: core/templates/core/password_reset_email.jinja #, python-format msgid "" "You're receiving this email because you requested a password reset for your " @@ -2928,27 +2693,27 @@ msgstr "" "Vous avez reçu cet email parce que vous avez demandé une réinitialisation du " "mot de passe pour votre compte sur le site %(site_name)s." -#: core/templates/core/password_reset_email.jinja:8 +#: core/templates/core/password_reset_email.jinja msgid "Your username, in case you've forgotten: " msgstr "Votre nom d'utilisateur, en cas d'oubli :" -#: core/templates/core/password_reset_email.jinja:10 +#: core/templates/core/password_reset_email.jinja msgid "Thanks for using our site! " msgstr "Merci d'utiliser notre site !" -#: core/templates/core/password_reset_email.jinja:12 +#: core/templates/core/password_reset_email.jinja #, python-format msgid "The %(site_name)s team" msgstr "L'équipe de %(site_name)s" -#: core/templates/core/register_confirm_mail.jinja:2 +#: core/templates/core/register_confirm_mail.jinja msgid "" "You're receiving this email because you created an account on the AE website." msgstr "" "Vous avez reçu cet email parce que vous avez créé un compte sur le site web " "de l'Association des Étudiants de l'UTBM." -#: core/templates/core/register_confirm_mail.jinja:6 +#: core/templates/core/register_confirm_mail.jinja msgid "" "\n" " As this is the website of the students of the AE, by the students of the " @@ -2966,318 +2731,309 @@ msgstr "" "personne, soit par mail à l'adresse ae@utbm.fr.\n" " " -#: core/templates/core/register_confirm_mail.jinja:12 +#: core/templates/core/register_confirm_mail.jinja msgid "Wishing you a good experience among us! " msgstr "En vous souhaitant une bonne expérience parmi nous !" -#: core/templates/core/search.jinja:6 +#: core/templates/core/search.jinja msgid "Search result" msgstr "Résultat de la recherche" -#: core/templates/core/search.jinja:10 +#: core/templates/core/search.jinja msgid "Users" msgstr "Utilisateurs" -#: core/templates/core/search.jinja:20 core/views/user.py:246 +#: core/templates/core/search.jinja core/views/user.py msgid "Clubs" msgstr "Clubs" -#: core/templates/core/user_account.jinja:8 +#: core/templates/core/user_account.jinja msgid "Year" msgstr "Année" -#: core/templates/core/user_account.jinja:9 +#: core/templates/core/user_account.jinja msgid "Month" msgstr "Mois" -#: core/templates/core/user_account.jinja:33 -#: core/templates/core/user_account_detail.jinja:4 +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja #, python-format msgid "%(user_name)s's account" msgstr "Compte de %(user_name)s" -#: core/templates/core/user_account.jinja:38 -#: core/templates/core/user_account_detail.jinja:8 +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja msgid "User account" msgstr "Compte utilisateur" -#: core/templates/core/user_account.jinja:42 -#: core/templates/core/user_account_detail.jinja:12 +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja msgid "Account purchases" msgstr "Achats du compte" -#: core/templates/core/user_account.jinja:46 -#: core/templates/core/user_account_detail.jinja:51 -#: counter/templates/counter/cash_summary_list.jinja:17 -#: counter/templates/counter/last_ops.jinja:16 +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja +#: counter/templates/counter/cash_summary_list.jinja +#: counter/templates/counter/last_ops.jinja msgid "Reloads" msgstr "Rechargements" -#: core/templates/core/user_account.jinja:50 -#: core/templates/core/user_account_detail.jinja:87 +#: core/templates/core/user_account.jinja +#: core/templates/core/user_account_detail.jinja msgid "Eboutic invoices" msgstr "Facture eboutic" -#: core/templates/core/user_account.jinja:54 -#: core/templates/core/user_tools.jinja:58 counter/views/mixins.py:114 +#: core/templates/core/user_account.jinja core/templates/core/user_tools.jinja +#: counter/views/mixins.py msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:631 +#: core/templates/core/user_account.jinja core/views/user.py msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" -#: core/templates/core/user_account_detail.jinja:92 +#: core/templates/core/user_account_detail.jinja msgid "Items" msgstr "Articles" -#: core/templates/core/user_clubs.jinja:4 +#: core/templates/core/user_clubs.jinja #, python-format msgid "%(user_name)s's club(s)" msgstr "Clubs de %(user_name)s" -#: core/templates/core/user_clubs.jinja:8 +#: core/templates/core/user_clubs.jinja msgid "Club(s)" msgstr "Clubs" -#: core/templates/core/user_clubs.jinja:10 +#: core/templates/core/user_clubs.jinja msgid "Current club(s) :" msgstr "Clubs actuels : " -#: core/templates/core/user_clubs.jinja:41 +#: core/templates/core/user_clubs.jinja msgid "Old club(s) :" msgstr "Anciens clubs :" -#: core/templates/core/user_clubs.jinja:74 +#: core/templates/core/user_clubs.jinja msgid "Subscribed mailing lists" msgstr "Mailing listes abonnées" -#: core/templates/core/user_clubs.jinja:76 +#: core/templates/core/user_clubs.jinja msgid "Unsubscribe" msgstr "Se désabonner" -#: core/templates/core/user_detail.jinja:9 +#: core/templates/core/user_detail.jinja #, python-format msgid "%(user_name)s's profile" msgstr "Profil de %(user_name)s" -#: core/templates/core/user_detail.jinja:34 +#: core/templates/core/user_detail.jinja msgid "Pronouns: " msgstr "Pronoms : " -#: core/templates/core/user_detail.jinja:40 +#: core/templates/core/user_detail.jinja msgid "Born: " msgstr "Né le : " -#: core/templates/core/user_detail.jinja:47 +#: core/templates/core/user_detail.jinja msgid "Department: " msgstr "Département : " -#: core/templates/core/user_detail.jinja:55 +#: core/templates/core/user_detail.jinja msgid "Option: " msgstr "Filière : " -#: core/templates/core/user_detail.jinja:62 -#: trombi/templates/trombi/export.jinja:20 +#: core/templates/core/user_detail.jinja trombi/templates/trombi/export.jinja msgid "Phone: " msgstr "Téléphone : " -#: core/templates/core/user_detail.jinja:69 +#: core/templates/core/user_detail.jinja msgid "Address: " msgstr "Adresse : " -#: core/templates/core/user_detail.jinja:76 +#: core/templates/core/user_detail.jinja msgid "Parents address: " msgstr "Adresse des parents : " -#: core/templates/core/user_detail.jinja:85 +#: core/templates/core/user_detail.jinja msgid "Promo: " msgstr "Promo : " -#: core/templates/core/user_detail.jinja:117 -#: core/templates/core/user_detail.jinja:118 -#: core/templates/core/user_detail.jinja:120 -#: core/templates/core/user_detail.jinja:121 +#: core/templates/core/user_detail.jinja msgid "Avatar" msgstr "Avatar" -#: core/templates/core/user_detail.jinja:125 -#: core/templates/core/user_detail.jinja:126 -#: core/templates/core/user_detail.jinja:128 -#: core/templates/core/user_detail.jinja:129 +#: core/templates/core/user_detail.jinja msgid "Scrub" msgstr "Blouse" -#: core/templates/core/user_detail.jinja:159 +#: core/templates/core/user_detail.jinja msgid "Not subscribed" msgstr "Non cotisant" -#: core/templates/core/user_detail.jinja:162 -#: subscription/templates/subscription/subscription.jinja:6 -#: subscription/templates/subscription/subscription.jinja:36 +#: core/templates/core/user_detail.jinja +#: subscription/templates/subscription/subscription.jinja msgid "New subscription" msgstr "Nouvelle cotisation" -#: core/templates/core/user_detail.jinja:173 +#: core/templates/core/user_detail.jinja msgid "Subscription history" msgstr "Historique de cotisation" -#: core/templates/core/user_detail.jinja:183 +#: core/templates/core/user_detail.jinja msgid "Subscription start" msgstr "Début de la cotisation" -#: core/templates/core/user_detail.jinja:184 +#: core/templates/core/user_detail.jinja msgid "Subscription end" msgstr "Fin de la cotisation" -#: core/templates/core/user_detail.jinja:185 -#: subscription/templates/subscription/stats.jinja:38 +#: core/templates/core/user_detail.jinja +#: subscription/templates/subscription/stats.jinja msgid "Subscription type" msgstr "Type de cotisation" -#: core/templates/core/user_detail.jinja:209 +#: core/templates/core/user_detail.jinja msgid "Give gift" msgstr "Donner cadeau" -#: core/templates/core/user_detail.jinja:217 +#: core/templates/core/user_detail.jinja msgid "Last given gift :" msgstr "Dernier cadeau donné :" -#: core/templates/core/user_detail.jinja:235 +#: core/templates/core/user_detail.jinja msgid "No gift given yet" msgstr "Aucun cadeau donné pour l'instant" -#: core/templates/core/user_edit.jinja:4 +#: core/templates/core/user_edit.jinja msgid "Edit user" msgstr "Éditer l'utilisateur" -#: core/templates/core/user_edit.jinja:41 +#: core/templates/core/user_edit.jinja msgid "Enable camera" msgstr "Activer la caméra" -#: core/templates/core/user_edit.jinja:49 +#: core/templates/core/user_edit.jinja msgid "Take a picture" msgstr "Prendre une photo" -#: core/templates/core/user_edit.jinja:67 +#: core/templates/core/user_edit.jinja msgid "To edit your profile picture, ask a member of the AE" msgstr "Pour changer votre photo de profil, demandez à un membre de l'AE" -#: core/templates/core/user_edit.jinja:96 +#: core/templates/core/user_edit.jinja msgid "Edit user profile" msgstr "Éditer le profil de l'utilisateur" -#: core/templates/core/user_edit.jinja:157 +#: core/templates/core/user_edit.jinja msgid "Change my password" msgstr "Changer mon mot de passe" -#: core/templates/core/user_edit.jinja:162 +#: core/templates/core/user_edit.jinja msgid "Change user password" msgstr "Changer le mot de passe" -#: core/templates/core/user_edit.jinja:173 +#: core/templates/core/user_edit.jinja msgid "Username:" msgstr "Nom d'utilisateur : " -#: core/templates/core/user_edit.jinja:176 +#: core/templates/core/user_edit.jinja msgid "Account number:" msgstr "Numéro de compte : " -#: core/templates/core/user_godfathers.jinja:9 +#: core/templates/core/user_godfathers.jinja #, python-format msgid "%(user_name)s's family" msgstr "Famille de %(user_name)s" -#: core/templates/core/user_godfathers.jinja:20 +#: core/templates/core/user_godfathers.jinja msgid "Show family tree" msgstr "Afficher l'arbre généalogique" -#: core/templates/core/user_godfathers.jinja:24 +#: core/templates/core/user_godfathers.jinja msgid "Godfathers / Godmothers" msgstr "Parrains / Marraines" -#: core/templates/core/user_godfathers.jinja:38 +#: core/templates/core/user_godfathers.jinja msgid "No godfathers / godmothers" msgstr "Pas de famille" -#: core/templates/core/user_godfathers.jinja:41 +#: core/templates/core/user_godfathers.jinja msgid "Godchildren" msgstr "Fillots / Fillotes" -#: core/templates/core/user_godfathers.jinja:54 +#: core/templates/core/user_godfathers.jinja msgid "No godchildren" msgstr "Pas de fillots / fillotes" -#: core/templates/core/user_godfathers_tree.jinja:14 +#: core/templates/core/user_godfathers_tree.jinja #, python-format msgid "%(user_name)s's family tree" msgstr "Arbre généalogique de %(user_name)s" -#: core/templates/core/user_godfathers_tree.jinja:23 +#: core/templates/core/user_godfathers_tree.jinja #, python-format msgid "Max godfather depth between %(min)s and %(max)s" msgstr "Maximum de profondeur pour les parrains entre %(min)s et %(max)s" -#: core/templates/core/user_godfathers_tree.jinja:49 +#: core/templates/core/user_godfathers_tree.jinja #, python-format msgid "Max godchildren depth between %(min)s and %(max)s" msgstr "Maximum de profondeur pour les fillots entre %(min)s et %(max)s" -#: core/templates/core/user_godfathers_tree.jinja:77 +#: core/templates/core/user_godfathers_tree.jinja msgid "Reverse" msgstr "Inverser" -#: core/templates/core/user_group.jinja:9 +#: core/templates/core/user_group.jinja #, python-format msgid "Edit user groups for %(user_name)s" msgstr "Éditer les groupes pour %(user_name)s" -#: core/templates/core/user_list.jinja:4 core/templates/core/user_list.jinja:8 +#: core/templates/core/user_list.jinja msgid "User list" msgstr "Liste d'utilisateurs" -#: core/templates/core/user_pictures.jinja:12 +#: core/templates/core/user_pictures.jinja #, python-format msgid "%(user_name)s's pictures" msgstr "Photos de %(user_name)s" -#: core/templates/core/user_pictures.jinja:25 +#: core/templates/core/user_pictures.jinja msgid "Download all my pictures" msgstr "Télécharger toutes mes photos" -#: core/templates/core/user_pictures.jinja:45 sas/templates/sas/album.jinja:78 -#: sas/templates/sas/macros.jinja:16 +#: core/templates/core/user_pictures.jinja sas/templates/sas/album.jinja +#: sas/templates/sas/macros.jinja msgid "To be moderated" msgstr "A modérer" -#: core/templates/core/user_preferences.jinja:8 -#: core/templates/core/user_preferences.jinja:13 core/views/user.py:238 +#: core/templates/core/user_preferences.jinja core/views/user.py msgid "Preferences" msgstr "Préférences" -#: core/templates/core/user_preferences.jinja:14 +#: core/templates/core/user_preferences.jinja msgid "General" msgstr "Général" -#: core/templates/core/user_preferences.jinja:21 trombi/views.py:57 +#: core/templates/core/user_preferences.jinja trombi/views.py msgid "Trombi" msgstr "Trombi" -#: core/templates/core/user_preferences.jinja:31 +#: core/templates/core/user_preferences.jinja #, python-format msgid "You already choose to be in that Trombi: %(trombi)s." msgstr "Vous avez déjà choisi ce Trombi: %(trombi)s." -#: core/templates/core/user_preferences.jinja:33 +#: core/templates/core/user_preferences.jinja msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" -#: core/templates/core/user_preferences.jinja:39 -#: counter/templates/counter/counter_click.jinja:180 +#: core/templates/core/user_preferences.jinja +#: counter/templates/counter/counter_click.jinja msgid "Student card" msgstr "Carte étudiante" -#: core/templates/core/user_preferences.jinja:42 +#: core/templates/core/user_preferences.jinja msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" @@ -3289,214 +3045,207 @@ msgstr "" "aurez besoin d'un lecteur NFC. Nous enregistrons l'UID de la carte qui fait " "14 caractères de long." -#: core/templates/core/user_stats.jinja:8 +#: core/templates/core/user_stats.jinja #, python-format msgid "%(user_name)s's stats" msgstr "Stats de %(user_name)s" -#: core/templates/core/user_stats.jinja:16 +#: core/templates/core/user_stats.jinja msgid "Permanencies" msgstr "Permanences" -#: core/templates/core/user_stats.jinja:27 +#: core/templates/core/user_stats.jinja msgid "Buyings" msgstr "Achats" -#: core/templates/core/user_stats.jinja:39 +#: core/templates/core/user_stats.jinja msgid "Product top 10" msgstr "Top 10 produits" -#: core/templates/core/user_stats.jinja:43 +#: core/templates/core/user_stats.jinja msgid "Product" msgstr "Produit" -#: core/templates/core/user_tools.jinja:8 +#: core/templates/core/user_tools.jinja #, python-format msgid "%(user_name)s's tools" msgstr "Outils de %(user_name)s" -#: core/templates/core/user_tools.jinja:13 +#: core/templates/core/user_tools.jinja msgid "User Tools" msgstr "Outils utilisateurs" -#: core/templates/core/user_tools.jinja:18 +#: core/templates/core/user_tools.jinja msgid "Sith management" msgstr "Gestion de Sith" -#: core/templates/core/user_tools.jinja:21 core/views/forms.py:293 -#: core/views/user.py:254 +#: core/templates/core/user_tools.jinja core/views/forms.py core/views/user.py msgid "Groups" msgstr "Groupes" -#: core/templates/core/user_tools.jinja:22 -#: rootplace/templates/rootplace/merge.jinja:4 +#: core/templates/core/user_tools.jinja +#: rootplace/templates/rootplace/merge.jinja msgid "Merge users" msgstr "Fusionner deux utilisateurs" -#: core/templates/core/user_tools.jinja:23 -#: rootplace/templates/rootplace/logs.jinja:5 +#: core/templates/core/user_tools.jinja +#: rootplace/templates/rootplace/logs.jinja msgid "Operation logs" msgstr "Journal d'opérations" -#: core/templates/core/user_tools.jinja:24 -#: rootplace/templates/rootplace/delete_user_messages.jinja:4 +#: core/templates/core/user_tools.jinja +#: rootplace/templates/rootplace/delete_user_messages.jinja msgid "Delete user's forum messages" msgstr "Supprimer les messages forum d'un utilisateur" -#: core/templates/core/user_tools.jinja:27 +#: core/templates/core/user_tools.jinja msgid "Subscriptions" msgstr "Cotisations" -#: core/templates/core/user_tools.jinja:30 -#: subscription/templates/subscription/stats.jinja:4 +#: core/templates/core/user_tools.jinja +#: subscription/templates/subscription/stats.jinja msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:182 -#: counter/views/mixins.py:89 +#: core/templates/core/user_tools.jinja counter/forms.py +#: counter/views/mixins.py msgid "Counters" msgstr "Comptoirs" -#: core/templates/core/user_tools.jinja:53 +#: core/templates/core/user_tools.jinja msgid "General counters management" msgstr "Gestion générale des comptoirs" -#: core/templates/core/user_tools.jinja:54 +#: core/templates/core/user_tools.jinja msgid "Products management" msgstr "Gestion des produits" -#: core/templates/core/user_tools.jinja:55 +#: core/templates/core/user_tools.jinja msgid "Product types management" msgstr "Gestion des types de produit" -#: core/templates/core/user_tools.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:23 -#: counter/views/mixins.py:104 +#: core/templates/core/user_tools.jinja +#: counter/templates/counter/cash_summary_list.jinja counter/views/mixins.py msgid "Cash register summaries" msgstr "Relevés de caisse" -#: core/templates/core/user_tools.jinja:57 -#: counter/templates/counter/invoices_call.jinja:4 counter/views/mixins.py:109 +#: core/templates/core/user_tools.jinja +#: counter/templates/counter/invoices_call.jinja counter/views/mixins.py msgid "Invoices call" msgstr "Appels à facture" -#: core/templates/core/user_tools.jinja:72 core/views/user.py:276 -#: counter/templates/counter/counter_list.jinja:18 -#: counter/templates/counter/counter_list.jinja:34 -#: counter/templates/counter/counter_list.jinja:50 +#: core/templates/core/user_tools.jinja core/views/user.py +#: counter/templates/counter/counter_list.jinja msgid "Stats" msgstr "Stats" -#: core/templates/core/user_tools.jinja:93 +#: core/templates/core/user_tools.jinja msgid "Refound Account" msgstr "Rembourser un compte" -#: core/templates/core/user_tools.jinja:94 +#: core/templates/core/user_tools.jinja msgid "General accounting" msgstr "Comptabilité générale" -#: core/templates/core/user_tools.jinja:109 +#: core/templates/core/user_tools.jinja msgid "Club account: " msgstr "Compte club : " -#: core/templates/core/user_tools.jinja:125 +#: core/templates/core/user_tools.jinja msgid "Communication" msgstr "Communication" -#: core/templates/core/user_tools.jinja:128 +#: core/templates/core/user_tools.jinja msgid "Create weekmail article" msgstr "Rédiger un nouvel article dans le Weekmail" -#: core/templates/core/user_tools.jinja:132 +#: core/templates/core/user_tools.jinja msgid "Moderate news" msgstr "Modérer les nouvelles" -#: core/templates/core/user_tools.jinja:133 +#: core/templates/core/user_tools.jinja msgid "Edit alert message" msgstr "Éditer le message d'alerte" -#: core/templates/core/user_tools.jinja:134 +#: core/templates/core/user_tools.jinja msgid "Edit information message" msgstr "Éditer le message d'informations" -#: core/templates/core/user_tools.jinja:135 +#: core/templates/core/user_tools.jinja msgid "Moderate files" msgstr "Modérer les fichiers" -#: core/templates/core/user_tools.jinja:141 +#: core/templates/core/user_tools.jinja msgid "Moderate pictures" msgstr "Modérer les photos" -#: core/templates/core/user_tools.jinja:165 -#: pedagogy/templates/pedagogy/guide.jinja:25 +#: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja msgid "Create UV" msgstr "Créer UV" -#: core/templates/core/user_tools.jinja:166 -#: pedagogy/templates/pedagogy/guide.jinja:28 -#: trombi/templates/trombi/detail.jinja:10 +#: core/templates/core/user_tools.jinja pedagogy/templates/pedagogy/guide.jinja +#: trombi/templates/trombi/detail.jinja msgid "Moderate comments" msgstr "Modérer les commentaires" -#: core/templates/core/user_tools.jinja:174 +#: core/templates/core/user_tools.jinja msgid "See available elections" msgstr "Voir les élections disponibles" -#: core/templates/core/user_tools.jinja:175 +#: core/templates/core/user_tools.jinja msgid "See archived elections" msgstr "Voir les élections archivées" -#: core/templates/core/user_tools.jinja:177 +#: core/templates/core/user_tools.jinja msgid "Create a new election" msgstr "Créer une nouvelle élection" -#: core/templates/core/user_tools.jinja:183 +#: core/templates/core/user_tools.jinja msgid "Other tools" msgstr "Autres outils" -#: core/templates/core/user_tools.jinja:185 +#: core/templates/core/user_tools.jinja msgid "Trombi tools" msgstr "Outils Trombi" -#: core/templatetags/renderer.py:81 +#: core/templatetags/renderer.py #, python-format msgid "%(nb_days)d day, %(remainder)s" msgid_plural "%(nb_days)d days, %(remainder)s" msgstr[0] "" msgstr[1] "" -#: core/views/files.py:119 +#: core/views/files.py msgid "Add a new folder" msgstr "Ajouter un nouveau dossier" -#: core/views/files.py:139 +#: core/views/files.py #, python-format msgid "Error creating folder %(folder_name)s: %(msg)s" msgstr "Erreur de création du dossier %(folder_name)s : %(msg)s" -#: core/views/files.py:159 core/views/forms.py:268 core/views/forms.py:275 -#: sas/forms.py:60 +#: core/views/files.py core/views/forms.py sas/forms.py #, python-format msgid "Error uploading file %(file_name)s: %(msg)s" msgstr "Erreur d'envoi du fichier %(file_name)s : %(msg)s" -#: core/views/files.py:232 sas/forms.py:83 +#: core/views/files.py sas/forms.py msgid "Apply rights recursively" msgstr "Appliquer les droits récursivement" -#: core/views/forms.py:90 core/views/forms.py:98 +#: core/views/forms.py msgid "Choose file" msgstr "Choisir un fichier" -#: core/views/forms.py:114 core/views/forms.py:122 +#: core/views/forms.py msgid "Choose user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:154 +#: core/views/forms.py msgid "Username, email, or account number" msgstr "Nom d'utilisateur, email, ou numéro de compte AE" -#: core/views/forms.py:218 +#: core/views/forms.py msgid "" "Profile: you need to be visible on the picture, in order to be recognized (e." "g. by the barmen)" @@ -3504,286 +3253,284 @@ msgstr "" "Photo de profil: vous devez être visible sur la photo afin d'être reconnu " "(par exemple par les barmen)" -#: core/views/forms.py:223 +#: core/views/forms.py msgid "Avatar: used on the forum" msgstr "Avatar : utilisé sur le forum" -#: core/views/forms.py:227 +#: core/views/forms.py msgid "Scrub: let other know how your scrub looks like!" msgstr "Blouse : montrez aux autres à quoi ressemble votre blouse !" -#: core/views/forms.py:279 +#: core/views/forms.py msgid "Bad image format, only jpeg, png, webp and gif are accepted" msgstr "Mauvais format d'image, seuls les jpeg, png, webp et gif sont acceptés" -#: core/views/forms.py:305 +#: core/views/forms.py msgid "Godfather / Godmother" msgstr "Parrain / Marraine" -#: core/views/forms.py:306 +#: core/views/forms.py msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:311 counter/forms.py:80 trombi/views.py:151 +#: core/views/forms.py counter/forms.py trombi/views.py msgid "Select user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:325 +#: core/views/forms.py msgid "This user does not exist" msgstr "Cet utilisateur n'existe pas" -#: core/views/forms.py:327 +#: core/views/forms.py msgid "You cannot be related to yourself" msgstr "Vous ne pouvez pas être relié à vous-même" -#: core/views/forms.py:339 +#: core/views/forms.py #, python-format msgid "%s is already your godfather" msgstr "%s est déjà votre parrain/marraine" -#: core/views/forms.py:345 +#: core/views/forms.py #, python-format msgid "%s is already your godchild" msgstr "%s est déjà votre fillot/fillote" -#: core/views/group.py:39 +#: core/views/group.py msgid "Users to add to group" msgstr "Utilisateurs à ajouter au groupe" -#: core/views/group.py:48 +#: core/views/group.py msgid "Users to remove from group" msgstr "Utilisateurs à retirer du groupe" -#: core/views/user.py:183 +#: core/views/user.py msgid "We couldn't verify that this email actually exists" msgstr "Nous n'avons pas réussi à vérifier que cette adresse mail existe." -#: core/views/user.py:206 +#: core/views/user.py msgid "Family" msgstr "Famille" -#: core/views/user.py:211 sas/templates/sas/album.jinja:67 -#: trombi/templates/trombi/export.jinja:25 -#: trombi/templates/trombi/user_profile.jinja:11 +#: core/views/user.py sas/templates/sas/album.jinja +#: trombi/templates/trombi/export.jinja +#: trombi/templates/trombi/user_profile.jinja msgid "Pictures" msgstr "Photos" -#: core/views/user.py:219 +#: core/views/user.py msgid "Galaxy" msgstr "Galaxie" -#: counter/apps.py:28 sith/settings.py:413 sith/settings.py:420 +#: counter/apps.py sith/settings.py msgid "Check" msgstr "Chèque" -#: counter/apps.py:29 sith/settings.py:414 sith/settings.py:422 +#: counter/apps.py sith/settings.py msgid "Cash" msgstr "Espèces" -#: counter/apps.py:30 counter/models.py:836 sith/settings.py:416 -#: sith/settings.py:421 +#: counter/apps.py counter/models.py sith/settings.py msgid "Credit card" msgstr "Carte bancaire" -#: counter/apps.py:36 counter/models.py:509 counter/models.py:997 -#: counter/models.py:1033 launderette/models.py:32 +#: counter/apps.py counter/models.py launderette/models.py msgid "counter" msgstr "comptoir" -#: counter/forms.py:61 +#: counter/forms.py msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:110 +#: counter/forms.py msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/management/commands/dump_accounts.py:148 +#: counter/management/commands/dump_accounts.py msgid "Your AE account has been emptied" msgstr "Votre compte AE a été vidé" -#: counter/management/commands/dump_warning_mail.py:112 +#: counter/management/commands/dump_warning_mail.py msgid "Clearing of your AE account" msgstr "Vidange de votre compte AE" -#: counter/models.py:92 +#: counter/models.py msgid "account id" msgstr "numéro de compte" -#: counter/models.py:94 +#: counter/models.py msgid "recorded product" msgstr "produits consignés" -#: counter/models.py:99 +#: counter/models.py msgid "customer" msgstr "client" -#: counter/models.py:100 +#: counter/models.py msgid "customers" msgstr "clients" -#: counter/models.py:112 counter/views/click.py:117 +#: counter/models.py counter/views/click.py msgid "Not enough money" msgstr "Solde insuffisant" -#: counter/models.py:198 +#: counter/models.py msgid "First name" msgstr "Prénom" -#: counter/models.py:199 +#: counter/models.py msgid "Last name" msgstr "Nom de famille" -#: counter/models.py:200 +#: counter/models.py msgid "Address 1" msgstr "Adresse 1" -#: counter/models.py:201 +#: counter/models.py msgid "Address 2" msgstr "Adresse 2" -#: counter/models.py:202 +#: counter/models.py msgid "Zip code" msgstr "Code postal" -#: counter/models.py:203 +#: counter/models.py msgid "City" msgstr "Ville" -#: counter/models.py:204 +#: counter/models.py msgid "Country" msgstr "Pays" -#: counter/models.py:212 +#: counter/models.py msgid "Phone number" msgstr "Numéro de téléphone" -#: counter/models.py:254 +#: counter/models.py msgid "When the mail warning that the account was about to be dumped was sent." msgstr "Quand le mail d'avertissement de la vidange du compte a été envoyé." -#: counter/models.py:259 +#: counter/models.py msgid "Set this to True if the warning mail received an error" msgstr "Mettre à True si le mail a reçu une erreur" -#: counter/models.py:266 +#: counter/models.py msgid "The operation that emptied the account." msgstr "L'opération qui a vidé le compte." -#: counter/models.py:304 +#: counter/models.py msgid "A text that will be shown on the eboutic." msgstr "Un texte qui sera affiché sur l'eboutic." -#: counter/models.py:311 counter/models.py:337 +#: counter/models.py msgid "product type" msgstr "type du produit" -#: counter/models.py:344 +#: counter/models.py msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:345 +#: counter/models.py msgid "Initial cost of purchasing the product" msgstr "Coût initial d'achat du produit" -#: counter/models.py:347 +#: counter/models.py msgid "selling price" msgstr "prix de vente" -#: counter/models.py:349 +#: counter/models.py msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:350 +#: counter/models.py msgid "Price for barmen during their permanence" msgstr "Prix pour les barmen durant leur permanence" -#: counter/models.py:358 +#: counter/models.py msgid "icon" msgstr "icône" -#: counter/models.py:363 +#: counter/models.py msgid "limit age" msgstr "âge limite" -#: counter/models.py:364 +#: counter/models.py msgid "tray price" msgstr "prix plateau" -#: counter/models.py:366 +#: counter/models.py msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:368 election/models.py:50 +#: counter/models.py election/models.py msgid "archived" msgstr "archivé" -#: counter/models.py:371 counter/models.py:1131 +#: counter/models.py msgid "product" msgstr "produit" -#: counter/models.py:488 +#: counter/models.py msgid "products" msgstr "produits" -#: counter/models.py:491 +#: counter/models.py msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:493 +#: counter/models.py msgid "Bar" msgstr "Bar" -#: counter/models.py:493 +#: counter/models.py msgid "Office" msgstr "Bureau" -#: counter/models.py:496 +#: counter/models.py msgid "sellers" msgstr "vendeurs" -#: counter/models.py:504 launderette/models.py:178 +#: counter/models.py launderette/models.py msgid "token" msgstr "jeton" -#: counter/models.py:735 +#: counter/models.py msgid "bank" msgstr "banque" -#: counter/models.py:737 counter/models.py:839 +#: counter/models.py msgid "is validated" msgstr "est validé" -#: counter/models.py:742 +#: counter/models.py msgid "refilling" msgstr "rechargement" -#: counter/models.py:816 eboutic/models.py:249 +#: counter/models.py eboutic/models.py msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:817 counter/models.py:1111 eboutic/models.py:250 +#: counter/models.py eboutic/models.py msgid "quantity" msgstr "quantité" -#: counter/models.py:836 +#: counter/models.py msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:844 +#: counter/models.py msgid "selling" msgstr "vente" -#: counter/models.py:948 +#: counter/models.py msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:949 +#: counter/models.py #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:951 counter/models.py:964 +#: counter/models.py #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3795,177 +3542,174 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:1002 +#: counter/models.py msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:1005 +#: counter/models.py msgid "permanency" msgstr "permanence" -#: counter/models.py:1038 +#: counter/models.py msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1041 +#: counter/models.py msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1107 +#: counter/models.py msgid "cash summary" msgstr "relevé" -#: counter/models.py:1110 +#: counter/models.py msgid "value" msgstr "valeur" -#: counter/models.py:1113 +#: counter/models.py msgid "check" msgstr "chèque" -#: counter/models.py:1115 +#: counter/models.py msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1119 +#: counter/models.py msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1135 +#: counter/models.py msgid "banner" msgstr "bannière" -#: counter/models.py:1137 +#: counter/models.py msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1139 +#: counter/models.py msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1141 +#: counter/models.py msgid "secret" msgstr "secret" -#: counter/models.py:1180 +#: counter/models.py msgid "uid" msgstr "uid" -#: counter/models.py:1185 counter/models.py:1190 +#: counter/models.py msgid "student card" msgstr "carte étudiante" -#: counter/models.py:1191 +#: counter/models.py msgid "student cards" msgstr "cartes étudiantes" -#: counter/templates/counter/activity.jinja:5 -#: counter/templates/counter/activity.jinja:13 +#: counter/templates/counter/activity.jinja #, python-format msgid "%(counter_name)s activity" msgstr "Activité sur %(counter_name)s" -#: counter/templates/counter/activity.jinja:15 +#: counter/templates/counter/activity.jinja msgid "Barmen list" msgstr "Barmans" -#: counter/templates/counter/activity.jinja:22 +#: counter/templates/counter/activity.jinja msgid "There is currently no barman connected." msgstr "Il n'y a actuellement aucun barman connecté." -#: counter/templates/counter/activity.jinja:27 +#: counter/templates/counter/activity.jinja msgid "Legend" msgstr "Légende" -#: counter/templates/counter/activity.jinja:31 +#: counter/templates/counter/activity.jinja msgid "counter is open, there's at least one barman connected" msgstr "Le comptoir est ouvert, et il y a au moins un barman connecté" -#: counter/templates/counter/activity.jinja:35 +#: counter/templates/counter/activity.jinja msgid "counter is not open : no one is connected" msgstr "Le comptoir est fermé" -#: counter/templates/counter/cash_register_summary.jinja:14 +#: counter/templates/counter/cash_register_summary.jinja msgid "Make a cash register summary" msgstr "Faire un relevé de caisse" -#: counter/templates/counter/cash_register_summary.jinja:28 +#: counter/templates/counter/cash_register_summary.jinja msgid "Are you sure ?" msgstr "Êtes vous sûr?" -#: counter/templates/counter/cash_summary_list.jinja:5 -#: counter/templates/counter/cash_summary_list.jinja:10 +#: counter/templates/counter/cash_summary_list.jinja msgid "Cash register summary list" msgstr "Liste des relevés de caisse" -#: counter/templates/counter/cash_summary_list.jinja:11 +#: counter/templates/counter/cash_summary_list.jinja msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 -#: counter/views/cash.py:88 +#: counter/templates/counter/cash_summary_list.jinja counter/views/cash.py msgid "Emptied" msgstr "Coffre vidé" -#: counter/templates/counter/cash_summary_list.jinja:48 +#: counter/templates/counter/cash_summary_list.jinja msgid "yes" msgstr "oui" -#: counter/templates/counter/cash_summary_list.jinja:63 +#: counter/templates/counter/cash_summary_list.jinja msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:48 -#: launderette/templates/launderette/launderette_admin.jinja:8 +#: counter/templates/counter/counter_click.jinja +#: launderette/templates/launderette/launderette_admin.jinja msgid "Selling" msgstr "Vente" -#: counter/templates/counter/counter_click.jinja:55 +#: counter/templates/counter/counter_click.jinja msgid "Select a product..." msgstr "Sélectionnez un produit…" -#: counter/templates/counter/counter_click.jinja:57 +#: counter/templates/counter/counter_click.jinja msgid "Operations" msgstr "Opérations" -#: counter/templates/counter/counter_click.jinja:58 +#: counter/templates/counter/counter_click.jinja msgid "Confirm (FIN)" msgstr "Confirmer (FIN)" -#: counter/templates/counter/counter_click.jinja:59 +#: counter/templates/counter/counter_click.jinja msgid "Cancel (ANN)" msgstr "Annuler (ANN)" -#: counter/templates/counter/counter_click.jinja:70 -#: counter/templates/counter/fragments/create_refill.jinja:8 -#: counter/templates/counter/fragments/create_student_card.jinja:10 -#: counter/templates/counter/invoices_call.jinja:16 -#: launderette/templates/launderette/launderette_admin.jinja:35 -#: launderette/templates/launderette/launderette_click.jinja:13 -#: sas/templates/sas/picture.jinja:167 -#: subscription/templates/subscription/stats.jinja:20 +#: counter/templates/counter/counter_click.jinja +#: counter/templates/counter/fragments/create_refill.jinja +#: counter/templates/counter/fragments/create_student_card.jinja +#: counter/templates/counter/invoices_call.jinja +#: launderette/templates/launderette/launderette_admin.jinja +#: launderette/templates/launderette/launderette_click.jinja +#: sas/templates/sas/picture.jinja +#: subscription/templates/subscription/stats.jinja msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:78 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:19 +#: counter/templates/counter/counter_click.jinja +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Basket: " msgstr "Panier : " -#: counter/templates/counter/counter_click.jinja:97 +#: counter/templates/counter/counter_click.jinja msgid "This basket is empty" msgstr "Votre panier est vide" -#: counter/templates/counter/counter_click.jinja:150 +#: counter/templates/counter/counter_click.jinja msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:161 -#: counter/templates/counter/refilling_list.jinja:9 +#: counter/templates/counter/counter_click.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Refilling" msgstr "Rechargement" -#: counter/templates/counter/counter_click.jinja:171 +#: counter/templates/counter/counter_click.jinja msgid "" "As a barman, you are not able to refill any account on your own. An admin " "should be connected on this counter for that. The customer can refill by " @@ -3975,127 +3719,122 @@ msgstr "" "vous même. Un admin doit être connecté sur ce comptoir pour cela. Le client " "peut recharger son compte en utilisant l'eboutic" -#: counter/templates/counter/counter_click.jinja:192 +#: counter/templates/counter/counter_click.jinja msgid "No products available on this counter for this user" msgstr "Pas de produits disponnibles dans ce comptoir pour cet utilisateur" -#: counter/templates/counter/counter_list.jinja:4 -#: counter/templates/counter/counter_list.jinja:10 +#: counter/templates/counter/counter_list.jinja msgid "Counter admin list" msgstr "Liste des comptoirs" -#: counter/templates/counter/counter_list.jinja:8 +#: counter/templates/counter/counter_list.jinja msgid "New counter" msgstr "Nouveau comptoir" -#: counter/templates/counter/counter_list.jinja:22 -#: counter/templates/counter/counter_list.jinja:38 -#: counter/templates/counter/refilling_list.jinja:5 +#: counter/templates/counter/counter_list.jinja +#: counter/templates/counter/refilling_list.jinja msgid "Reloads list" msgstr "Liste de rechargements" -#: counter/templates/counter/counter_list.jinja:27 +#: counter/templates/counter/counter_list.jinja msgid "Bars" msgstr "Bars" -#: counter/templates/counter/counter_list.jinja:43 +#: counter/templates/counter/counter_list.jinja msgid "Offices" msgstr "Bureaux" -#: counter/templates/counter/counter_list.jinja:59 +#: counter/templates/counter/counter_list.jinja msgid "There is no counters in this website." msgstr "Il n'y a pas de comptoirs dans ce site web." -#: counter/templates/counter/counter_main.jinja:12 -#: counter/templates/counter/counter_main.jinja:22 -#: launderette/templates/launderette/launderette_click.jinja:8 +#: counter/templates/counter/counter_main.jinja +#: launderette/templates/launderette/launderette_click.jinja #, python-format msgid "%(counter_name)s counter" msgstr "Comptoir %(counter_name)s" -#: counter/templates/counter/counter_main.jinja:26 +#: counter/templates/counter/counter_main.jinja msgid "Last selling: " msgstr "Dernière vente : " -#: counter/templates/counter/counter_main.jinja:27 +#: counter/templates/counter/counter_main.jinja msgid "Client: " msgstr "Client : " -#: counter/templates/counter/counter_main.jinja:27 +#: counter/templates/counter/counter_main.jinja msgid "New amount: " msgstr "Nouveau montant : " -#: counter/templates/counter/counter_main.jinja:36 +#: counter/templates/counter/counter_main.jinja msgid "Enter client code:" msgstr "Entrez un code client : " -#: counter/templates/counter/counter_main.jinja:41 +#: counter/templates/counter/counter_main.jinja msgid "validate" msgstr "valider" -#: counter/templates/counter/counter_main.jinja:44 +#: counter/templates/counter/counter_main.jinja msgid "Please, login" msgstr "Merci de vous identifier" -#: counter/templates/counter/counter_main.jinja:49 +#: counter/templates/counter/counter_main.jinja msgid "Barman: " msgstr "Barman : " -#: counter/templates/counter/counter_main.jinja:56 +#: counter/templates/counter/counter_main.jinja msgid "login" msgstr "login" -#: counter/templates/counter/eticket_list.jinja:4 -#: counter/templates/counter/eticket_list.jinja:10 +#: counter/templates/counter/eticket_list.jinja msgid "Eticket list" msgstr "Liste des etickets" -#: counter/templates/counter/eticket_list.jinja:8 +#: counter/templates/counter/eticket_list.jinja msgid "New eticket" msgstr "Nouveau eticket" -#: counter/templates/counter/eticket_list.jinja:17 +#: counter/templates/counter/eticket_list.jinja msgid "There is no eticket in this website." msgstr "Il n'y a pas de eticket sur ce site web." -#: counter/templates/counter/fragments/create_student_card.jinja:12 +#: counter/templates/counter/fragments/create_student_card.jinja msgid "No student card registered." msgstr "Aucune carte étudiante enregistrée." -#: counter/templates/counter/fragments/create_student_card.jinja:15 +#: counter/templates/counter/fragments/create_student_card.jinja #, python-format msgid "uid: %(uid)s " msgstr "uid: %(uid)s" -#: counter/templates/counter/fragments/create_student_card.jinja:16 +#: counter/templates/counter/fragments/create_student_card.jinja msgid "Card registered" msgstr "Carte enregistrée" -#: counter/templates/counter/invoices_call.jinja:8 +#: counter/templates/counter/invoices_call.jinja #, python-format msgid "Invoices call for %(date)s" msgstr "Appels à facture pour %(date)s" -#: counter/templates/counter/invoices_call.jinja:9 +#: counter/templates/counter/invoices_call.jinja msgid "Choose another month: " msgstr "Choisir un autre mois : " -#: counter/templates/counter/invoices_call.jinja:19 +#: counter/templates/counter/invoices_call.jinja msgid "CB Payments" msgstr "Payements en Carte Bancaire" -#: counter/templates/counter/last_ops.jinja:5 -#: counter/templates/counter/last_ops.jinja:15 +#: counter/templates/counter/last_ops.jinja #, python-format msgid "%(counter_name)s last operations" msgstr "Dernières opérations sur %(counter_name)s" -#: counter/templates/counter/mails/account_dump.jinja:1 -#: counter/templates/counter/mails/account_dump_warning.jinja:1 +#: counter/templates/counter/mails/account_dump.jinja +#: counter/templates/counter/mails/account_dump_warning.jinja msgid "Hello" msgstr "Bonjour" -#: counter/templates/counter/mails/account_dump.jinja:3 +#: counter/templates/counter/mails/account_dump.jinja #, python-format msgid "" "Following the email we sent you on %(date)s, the money of your AE account " @@ -4104,13 +3843,13 @@ msgstr "" "Suite au mail que nous vous avions envoyé le %(date)s, l'argent de votre " "compte AE (%(amount)s €) a été rendu à l'AE." -#: counter/templates/counter/mails/account_dump.jinja:6 +#: counter/templates/counter/mails/account_dump.jinja msgid "If you think this was a mistake, please mail us at ae@utbm.fr." msgstr "" "Si vous pensez qu'il s'agit d'une erreur, veuillez envoyer un mail à ae@utbm." "fr." -#: counter/templates/counter/mails/account_dump.jinja:8 +#: counter/templates/counter/mails/account_dump.jinja msgid "" "Please mind that this is not a closure of your account. You can still access " "it via the AE website. You are also still able to renew your subscription." @@ -4118,12 +3857,12 @@ msgstr "" "Il ne s'agit pas d'une fermeture de votre compte. Vous pouvez toujours y " "accéder via le site AE. Vous êtes également toujours en mesure de re-cotiser." -#: counter/templates/counter/mails/account_dump.jinja:12 -#: counter/templates/counter/mails/account_dump_warning.jinja:26 +#: counter/templates/counter/mails/account_dump.jinja +#: counter/templates/counter/mails/account_dump_warning.jinja msgid "Sincerely" msgstr "Cordialement" -#: counter/templates/counter/mails/account_dump_warning.jinja:3 +#: counter/templates/counter/mails/account_dump_warning.jinja #, python-format msgid "" "You received this email because your last subscription to the Students' " @@ -4132,7 +3871,7 @@ msgstr "" "Vous recevez ce mail car votre dernière cotisation à l'assocation des " "étudiants de l'UTBM s'est achevée le %(date)s." -#: counter/templates/counter/mails/account_dump_warning.jinja:6 +#: counter/templates/counter/mails/account_dump_warning.jinja #, python-format msgid "" "In accordance with the Internal Regulations, the balance of any inactive AE " @@ -4145,7 +3884,7 @@ msgstr "" "compte sera donc récupéré en totalité le %(date)s, pour un total de " "%(amount)s €. " -#: counter/templates/counter/mails/account_dump_warning.jinja:12 +#: counter/templates/counter/mails/account_dump_warning.jinja msgid "" "However, if your subscription is renewed by this date, your right to keep " "the money in your AE account will be renewed." @@ -4153,7 +3892,7 @@ msgstr "" "Cependant, si votre cotisation est renouvelée d'ici cette date, votre droit " "à conserver l'argent de votre compte AE sera renouvelé." -#: counter/templates/counter/mails/account_dump_warning.jinja:16 +#: counter/templates/counter/mails/account_dump_warning.jinja msgid "" "You can also request a refund by sending an email to ae@utbm.fr before the " "aforementioned date." @@ -4161,7 +3900,7 @@ msgstr "" "Vous pouvez également effectuer une demande de remboursement par mail à " "l'adresse ae@utbm.fr avant la date susmentionnée." -#: counter/templates/counter/mails/account_dump_warning.jinja:20 +#: counter/templates/counter/mails/account_dump_warning.jinja msgid "" "Whatever you decide, you won't be expelled from the association, and you " "won't lose your rights. You will always be able to renew your subscription " @@ -4173,54 +3912,52 @@ msgstr "" "votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura " "aucune conséquence autre que le retrait de l'argent de votre compte." -#: counter/templates/counter/product_list.jinja:5 -#: counter/templates/counter/product_list.jinja:62 +#: counter/templates/counter/product_list.jinja msgid "Product list" msgstr "Liste des produits" -#: counter/templates/counter/product_list.jinja:22 +#: counter/templates/counter/product_list.jinja msgid "Filter products" msgstr "Filtrer les produits" -#: counter/templates/counter/product_list.jinja:27 +#: counter/templates/counter/product_list.jinja msgid "Product name" msgstr "Nom du produit" -#: counter/templates/counter/product_list.jinja:36 +#: counter/templates/counter/product_list.jinja msgid "Product state" msgstr "Etat du produit" -#: counter/templates/counter/product_list.jinja:39 +#: counter/templates/counter/product_list.jinja msgid "Active products" msgstr "Produits actifs" -#: counter/templates/counter/product_list.jinja:47 +#: counter/templates/counter/product_list.jinja msgid "All products" msgstr "Tous les produits" -#: counter/templates/counter/product_list.jinja:52 +#: counter/templates/counter/product_list.jinja msgid "Product type" msgstr "Type de produit" -#: counter/templates/counter/product_list.jinja:66 +#: counter/templates/counter/product_list.jinja msgid "New product" msgstr "Nouveau produit" -#: counter/templates/counter/product_type_list.jinja:4 -#: counter/templates/counter/product_type_list.jinja:42 +#: counter/templates/counter/product_type_list.jinja msgid "Product type list" msgstr "Liste des types de produit" -#: counter/templates/counter/product_type_list.jinja:18 +#: counter/templates/counter/product_type_list.jinja msgid "New product type" msgstr "Nouveau type de produit" -#: counter/templates/counter/product_type_list.jinja:25 +#: counter/templates/counter/product_type_list.jinja msgid "Product types are in the same order on this page and on the eboutic." msgstr "" "Les types de produit sont dans le même ordre sur cette page et sur l'eboutic." -#: counter/templates/counter/product_type_list.jinja:28 +#: counter/templates/counter/product_type_list.jinja msgid "" "You can reorder them here by drag-and-drop. The changes will then be applied " "globally immediately." @@ -4228,206 +3965,200 @@ msgstr "" "Vous pouvez les réorganiser ici. Les changements seront alors immédiatement " "appliqués globalement." -#: counter/templates/counter/product_type_list.jinja:61 +#: counter/templates/counter/product_type_list.jinja msgid "There are no product types in this website." msgstr "Il n'y a pas de types de produit dans ce site web." -#: counter/templates/counter/refilling_list.jinja:15 +#: counter/templates/counter/refilling_list.jinja msgid "Seller" msgstr "Vendeur" -#: counter/templates/counter/stats.jinja:5 -#: counter/templates/counter/stats.jinja:13 +#: counter/templates/counter/stats.jinja #, python-format msgid "%(counter_name)s stats" msgstr "Stats sur %(counter_name)s" -#: counter/templates/counter/stats.jinja:15 +#: counter/templates/counter/stats.jinja #, python-format msgid "Top 100 %(counter_name)s" msgstr "Top 100 %(counter_name)s" -#: counter/templates/counter/stats.jinja:22 -#: counter/templates/counter/stats.jinja:48 -#: counter/templates/counter/stats.jinja:70 +#: counter/templates/counter/stats.jinja msgid "Promo" msgstr "Promo" -#: counter/templates/counter/stats.jinja:24 +#: counter/templates/counter/stats.jinja msgid "Percentage" msgstr "Pourcentage" -#: counter/templates/counter/stats.jinja:41 +#: counter/templates/counter/stats.jinja #, python-format msgid "Top 100 barman %(counter_name)s" msgstr "Top 100 barman %(counter_name)s" -#: counter/templates/counter/stats.jinja:49 -#: counter/templates/counter/stats.jinja:71 +#: counter/templates/counter/stats.jinja msgid "Time" msgstr "Temps" -#: counter/templates/counter/stats.jinja:64 +#: counter/templates/counter/stats.jinja #, python-format msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views/cash.py:45 +#: counter/views/cash.py msgid "10 cents" msgstr "10 centimes" -#: counter/views/cash.py:46 +#: counter/views/cash.py msgid "20 cents" msgstr "20 centimes" -#: counter/views/cash.py:47 +#: counter/views/cash.py msgid "50 cents" msgstr "50 centimes" -#: counter/views/cash.py:48 +#: counter/views/cash.py msgid "1 euro" msgstr "1 €" -#: counter/views/cash.py:49 +#: counter/views/cash.py msgid "2 euros" msgstr "2 €" -#: counter/views/cash.py:50 +#: counter/views/cash.py msgid "5 euros" msgstr "5 €" -#: counter/views/cash.py:51 +#: counter/views/cash.py msgid "10 euros" msgstr "10 €" -#: counter/views/cash.py:52 +#: counter/views/cash.py msgid "20 euros" msgstr "20 €" -#: counter/views/cash.py:53 +#: counter/views/cash.py msgid "50 euros" msgstr "50 €" -#: counter/views/cash.py:55 +#: counter/views/cash.py msgid "100 euros" msgstr "100 €" -#: counter/views/cash.py:58 counter/views/cash.py:64 counter/views/cash.py:70 -#: counter/views/cash.py:76 counter/views/cash.py:82 +#: counter/views/cash.py msgid "Check amount" msgstr "Montant du chèque" -#: counter/views/cash.py:61 counter/views/cash.py:67 counter/views/cash.py:73 -#: counter/views/cash.py:79 counter/views/cash.py:85 +#: counter/views/cash.py msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views/click.py:77 +#: counter/views/click.py msgid "The selected product isn't available for this user" msgstr "Le produit sélectionné n'est pas disponnible pour cet utilisateur" -#: counter/views/click.py:112 +#: counter/views/click.py msgid "Submmited basket is invalid" msgstr "Le panier envoyé est invalide" -#: counter/views/click.py:130 +#: counter/views/click.py msgid "This user have reached his recording limit" msgstr "Cet utilisateur a atteint sa limite de déconsigne" -#: counter/views/eticket.py:120 +#: counter/views/eticket.py msgid "people(s)" msgstr "personne(s)" -#: counter/views/home.py:77 +#: counter/views/home.py msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views/home.py:79 +#: counter/views/home.py msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views/home.py:84 +#: counter/views/home.py msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" -#: counter/views/mixins.py:68 +#: counter/views/mixins.py msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views/mixins.py:77 +#: counter/views/mixins.py msgid "Last operations" msgstr "Dernières opérations" -#: counter/views/mixins.py:84 +#: counter/views/mixins.py msgid "Counter administration" msgstr "Administration des comptoirs" -#: counter/views/mixins.py:99 +#: counter/views/mixins.py msgid "Product types" msgstr "Types de produit" -#: counter/views/student_card.py:54 +#: counter/views/student_card.py #, python-format msgid "%(name)s has no registered student card" msgstr "%(name)s n'a pas de carte étudiante enregistrée" -#: eboutic/forms.py:88 +#: eboutic/forms.py msgid "The request was badly formatted." msgstr "La requête a été mal formatée." -#: eboutic/forms.py:91 +#: eboutic/forms.py msgid "Your basket is empty." msgstr "Votre panier est vide" -#: eboutic/forms.py:101 +#: eboutic/forms.py #, python-format msgid "%(name)s : this product does not exist or may no longer be available." msgstr "%(name)s : ce produit n'existe pas ou n'est peut-être plus disponible." -#: eboutic/models.py:194 +#: eboutic/models.py msgid "validated" msgstr "validé" -#: eboutic/models.py:210 +#: eboutic/models.py msgid "Invoice already validated" msgstr "Facture déjà validée" -#: eboutic/models.py:246 +#: eboutic/models.py msgid "product id" msgstr "ID du produit" -#: eboutic/models.py:247 +#: eboutic/models.py msgid "product name" msgstr "nom du produit" -#: eboutic/models.py:248 +#: eboutic/models.py msgid "product type id" msgstr "id du type du produit" -#: eboutic/models.py:265 +#: eboutic/models.py msgid "basket" msgstr "panier" -#: eboutic/templates/eboutic/eboutic_main.jinja:40 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:44 +#: eboutic/templates/eboutic/eboutic_main.jinja +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Current account amount: " msgstr "Solde actuel : " -#: eboutic/templates/eboutic/eboutic_main.jinja:59 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:40 +#: eboutic/templates/eboutic/eboutic_main.jinja +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Basket amount: " msgstr "Valeur du panier : " -#: eboutic/templates/eboutic/eboutic_main.jinja:66 +#: eboutic/templates/eboutic/eboutic_main.jinja msgid "Clear" msgstr "Vider" -#: eboutic/templates/eboutic/eboutic_main.jinja:72 -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:94 +#: eboutic/templates/eboutic/eboutic_main.jinja +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Validate" msgstr "Valider" -#: eboutic/templates/eboutic/eboutic_main.jinja:81 +#: eboutic/templates/eboutic/eboutic_main.jinja msgid "" "You have not filled in your date of birth. As a result, you may not have " "access to all the products in the online shop. To fill in your date of " @@ -4438,27 +4169,27 @@ msgstr "" "boutique en ligne. Pour remplir votre date de naissance, vous pouvez aller " "sur" -#: eboutic/templates/eboutic/eboutic_main.jinja:83 +#: eboutic/templates/eboutic/eboutic_main.jinja msgid "this page" msgstr "cette page" -#: eboutic/templates/eboutic/eboutic_main.jinja:132 +#: eboutic/templates/eboutic/eboutic_main.jinja msgid "There are no items available for sale" msgstr "Aucun article n'est disponible à la vente" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:4 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Basket state" msgstr "État du panier" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:49 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Remaining account amount: " msgstr "Solde restant : " -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:64 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Billing information" msgstr "Informations de facturation" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:102 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" "You must fill your billing infos if you want to pay with your credit\n" " card" @@ -4466,7 +4197,7 @@ msgstr "" "Vous devez renseigner vos coordonnées de facturation si vous voulez payer " "par carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:107 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" "\n" " The Crédit Agricole changed its policy related to the " @@ -4484,564 +4215,543 @@ msgstr "" "souhaitez payer par carte, vous devez rajouter un numéro de téléphone aux " "données que vous aviez déjà fourni." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:123 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Pay with credit card" msgstr "Payer avec une carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:128 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" "AE account payment disabled because your basket contains refilling items." msgstr "" "Paiement par compte AE désactivé parce que votre panier contient des bons de " "rechargement." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:130 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" "AE account payment disabled because you do not have enough money remaining." msgstr "" "Paiement par compte AE désactivé parce que votre solde est insuffisant." -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:135 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Pay with Sith account" msgstr "Payer avec un compte AE" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:146 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Billing info registration success" msgstr "Informations de facturation enregistrées" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:147 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Billing info registration failure" msgstr "Echec de l'enregistrement des informations de facturation." -#: eboutic/templates/eboutic/eboutic_payment_result.jinja:8 +#: eboutic/templates/eboutic/eboutic_payment_result.jinja msgid "Payment successful" msgstr "Le paiement a été effectué" -#: eboutic/templates/eboutic/eboutic_payment_result.jinja:10 +#: eboutic/templates/eboutic/eboutic_payment_result.jinja msgid "Payment failed" msgstr "Le paiement a échoué" -#: eboutic/templates/eboutic/eboutic_payment_result.jinja:12 +#: eboutic/templates/eboutic/eboutic_payment_result.jinja msgid "Return to eboutic" msgstr "Retourner à l'eboutic" -#: election/models.py:14 +#: election/models.py msgid "start candidature" msgstr "début des candidatures" -#: election/models.py:15 +#: election/models.py msgid "end candidature" msgstr "fin des candidatures" -#: election/models.py:22 +#: election/models.py msgid "edit groups" msgstr "groupe d'édition" -#: election/models.py:29 +#: election/models.py msgid "view groups" msgstr "groupe de vue" -#: election/models.py:36 +#: election/models.py msgid "vote groups" msgstr "groupe de vote" -#: election/models.py:43 +#: election/models.py msgid "candidature groups" msgstr "groupe de candidature" -#: election/models.py:111 election/models.py:156 +#: election/models.py msgid "election" msgstr "élection" -#: election/models.py:116 +#: election/models.py msgid "max choice" msgstr "nombre de choix maxi" -#: election/models.py:192 +#: election/models.py msgid "election list" msgstr "liste électorale" -#: election/models.py:215 +#: election/models.py msgid "candidature" msgstr "candidature" -#: election/templates/election/candidate_form.jinja:4 -#: election/templates/election/candidate_form.jinja:13 -#: election/templates/election/election_detail.jinja:179 +#: election/templates/election/candidate_form.jinja +#: election/templates/election/election_detail.jinja msgid "Candidate" msgstr "Candidater" -#: election/templates/election/candidate_form.jinja:17 +#: election/templates/election/candidate_form.jinja msgid "Candidature are closed for this election" msgstr "Les candidatures sont fermées pour cette élection" -#: election/templates/election/election_detail.jinja:23 +#: election/templates/election/election_detail.jinja msgid "Polls close " msgstr "Votes fermés" -#: election/templates/election/election_detail.jinja:25 +#: election/templates/election/election_detail.jinja msgid "Polls closed " msgstr "Votes fermés" -#: election/templates/election/election_detail.jinja:27 +#: election/templates/election/election_detail.jinja msgid "Polls will open " msgstr "Les votes ouvriront " -#: election/templates/election/election_detail.jinja:29 -#: election/templates/election/election_detail.jinja:33 -#: election/templates/election/election_list.jinja:32 -#: election/templates/election/election_list.jinja:35 -#: election/templates/election/election_list.jinja:40 -#: election/templates/election/election_list.jinja:43 -#: forum/templates/forum/macros.jinja:158 +#: election/templates/election/election_detail.jinja +#: election/templates/election/election_list.jinja +#: forum/templates/forum/macros.jinja msgid " at " msgstr " à " -#: election/templates/election/election_detail.jinja:30 +#: election/templates/election/election_detail.jinja msgid "and will close " msgstr "et fermeront" -#: election/templates/election/election_detail.jinja:38 +#: election/templates/election/election_detail.jinja msgid "You already have submitted your vote." msgstr "Vous avez déjà soumis votre vote." -#: election/templates/election/election_detail.jinja:40 +#: election/templates/election/election_detail.jinja msgid "You have voted in this election." msgstr "Vous avez déjà voté pour cette élection." -#: election/templates/election/election_detail.jinja:53 election/views.py:98 +#: election/templates/election/election_detail.jinja election/views.py msgid "Blank vote" msgstr "Vote blanc" -#: election/templates/election/election_detail.jinja:75 +#: election/templates/election/election_detail.jinja msgid "You may choose up to" msgstr "Vous pouvez choisir jusqu'à" -#: election/templates/election/election_detail.jinja:75 +#: election/templates/election/election_detail.jinja msgid "people." msgstr "personne(s)" -#: election/templates/election/election_detail.jinja:112 +#: election/templates/election/election_detail.jinja msgid "Choose blank vote" msgstr "Choisir de voter blanc" -#: election/templates/election/election_detail.jinja:120 -#: election/templates/election/election_detail.jinja:163 +#: election/templates/election/election_detail.jinja msgid "votes" msgstr "votes" -#: election/templates/election/election_detail.jinja:182 +#: election/templates/election/election_detail.jinja msgid "Add a new list" msgstr "Ajouter une nouvelle liste" -#: election/templates/election/election_detail.jinja:186 +#: election/templates/election/election_detail.jinja msgid "Add a new role" msgstr "Ajouter un nouveau rôle" -#: election/templates/election/election_detail.jinja:196 +#: election/templates/election/election_detail.jinja msgid "Submit the vote !" msgstr "Envoyer le vote !" -#: election/templates/election/election_detail.jinja:205 -#: election/templates/election/election_detail.jinja:210 +#: election/templates/election/election_detail.jinja msgid "Show more" msgstr "Montrer plus" -#: election/templates/election/election_detail.jinja:206 -#: election/templates/election/election_detail.jinja:211 +#: election/templates/election/election_detail.jinja msgid "Show less" msgstr "Montrer moins" -#: election/templates/election/election_list.jinja:5 +#: election/templates/election/election_list.jinja msgid "Election list" msgstr "Liste des élections" -#: election/templates/election/election_list.jinja:22 +#: election/templates/election/election_list.jinja msgid "Current elections" msgstr "Élections actuelles" -#: election/templates/election/election_list.jinja:30 +#: election/templates/election/election_list.jinja msgid "Applications open from" msgstr "Candidatures ouvertes à partir du" -#: election/templates/election/election_list.jinja:33 -#: election/templates/election/election_list.jinja:41 +#: election/templates/election/election_list.jinja msgid "to" msgstr "au" -#: election/templates/election/election_list.jinja:38 +#: election/templates/election/election_list.jinja msgid "Polls open from" msgstr "Votes ouverts du" -#: election/views.py:45 +#: election/views.py msgid "You have selected too much candidates." msgstr "Vous avez sélectionné trop de candidats." -#: election/views.py:59 +#: election/views.py msgid "User to candidate" msgstr "Utilisateur se présentant" -#: election/views.py:124 +#: election/views.py msgid "This role already exists for this election" msgstr "Ce rôle existe déjà pour cette élection" -#: election/views.py:173 +#: election/views.py msgid "Start candidature" msgstr "Début des candidatures" -#: election/views.py:176 +#: election/views.py msgid "End candidature" msgstr "Fin des candidatures" -#: forum/models.py:62 +#: forum/models.py msgid "is a category" msgstr "est une catégorie" -#: forum/models.py:73 +#: forum/models.py msgid "owner club" msgstr "club propriétaire" -#: forum/models.py:90 +#: forum/models.py msgid "number to choose a specific forum ordering" msgstr "numéro spécifiant l'ordre d'affichage" -#: forum/models.py:95 forum/models.py:252 +#: forum/models.py msgid "the last message" msgstr "le dernier message" -#: forum/models.py:99 +#: forum/models.py msgid "number of topics" msgstr "nombre de sujets" -#: forum/models.py:195 +#: forum/models.py msgid "You can not make loops in forums" msgstr "Vous ne pouvez pas faire de boucles dans les forums" -#: forum/models.py:247 +#: forum/models.py msgid "subscribed users" msgstr "utilisateurs abonnés" -#: forum/models.py:257 +#: forum/models.py msgid "number of messages" msgstr "nombre de messages" -#: forum/models.py:311 +#: forum/models.py msgid "message" msgstr "message" -#: forum/models.py:314 +#: forum/models.py msgid "readers" msgstr "lecteurs" -#: forum/models.py:316 +#: forum/models.py msgid "is deleted" msgstr "est supprimé" -#: forum/models.py:400 +#: forum/models.py msgid "Message edited by" msgstr "Message édité par" -#: forum/models.py:401 +#: forum/models.py msgid "Message deleted by" msgstr "Message supprimé par" -#: forum/models.py:402 +#: forum/models.py msgid "Message undeleted by" msgstr "Message restauré par" -#: forum/models.py:414 +#: forum/models.py msgid "action" msgstr "action" -#: forum/models.py:437 +#: forum/models.py msgid "last read date" msgstr "dernière date de lecture" -#: forum/templates/forum/favorite_topics.jinja:5 -#: forum/templates/forum/favorite_topics.jinja:15 -#: forum/templates/forum/favorite_topics.jinja:19 -#: forum/templates/forum/main.jinja:21 +#: forum/templates/forum/favorite_topics.jinja forum/templates/forum/main.jinja msgid "Favorite topics" msgstr "Topics favoris" -#: forum/templates/forum/forum.jinja:22 forum/templates/forum/main.jinja:29 +#: forum/templates/forum/forum.jinja forum/templates/forum/main.jinja msgid "New forum" msgstr "Nouveau forum" -#: forum/templates/forum/forum.jinja:25 forum/templates/forum/reply.jinja:8 -#: forum/templates/forum/reply.jinja:33 +#: forum/templates/forum/forum.jinja forum/templates/forum/reply.jinja msgid "New topic" msgstr "Nouveau sujet" -#: forum/templates/forum/forum.jinja:36 forum/templates/forum/main.jinja:38 +#: forum/templates/forum/forum.jinja forum/templates/forum/main.jinja msgid "Topics" msgstr "Sujets" -#: forum/templates/forum/forum.jinja:39 forum/templates/forum/forum.jinja:61 -#: forum/templates/forum/main.jinja:41 +#: forum/templates/forum/forum.jinja forum/templates/forum/main.jinja msgid "Last message" msgstr "Dernier message" -#: forum/templates/forum/forum.jinja:58 +#: forum/templates/forum/forum.jinja msgid "Messages" msgstr "Messages" -#: forum/templates/forum/last_unread.jinja:5 -#: forum/templates/forum/last_unread.jinja:15 -#: forum/templates/forum/last_unread.jinja:19 +#: forum/templates/forum/last_unread.jinja msgid "Last unread messages" msgstr "Derniers messages non lus" -#: forum/templates/forum/last_unread.jinja:22 +#: forum/templates/forum/last_unread.jinja msgid "Refresh" msgstr "Rafraîchir" -#: forum/templates/forum/macros.jinja:133 +#: forum/templates/forum/macros.jinja msgid "Undelete" msgstr "Restaurer" -#: forum/templates/forum/macros.jinja:159 +#: forum/templates/forum/macros.jinja msgid " the " msgstr " le " -#: forum/templates/forum/macros.jinja:170 +#: forum/templates/forum/macros.jinja msgid "Deleted or unreadable message." msgstr "Message supprimé ou non-visible." -#: forum/templates/forum/macros.jinja:182 +#: forum/templates/forum/macros.jinja msgid "Order by date" msgstr "Trier par date" -#: forum/templates/forum/main.jinja:20 +#: forum/templates/forum/main.jinja msgid "View last unread messages" msgstr "Voir les derniers messages non lus" -#: forum/templates/forum/reply.jinja:6 forum/templates/forum/reply.jinja:30 -#: forum/templates/forum/topic.jinja:21 forum/templates/forum/topic.jinja:45 +#: forum/templates/forum/reply.jinja forum/templates/forum/topic.jinja msgid "Reply" msgstr "Répondre" -#: forum/templates/forum/search.jinja:22 +#: forum/templates/forum/search.jinja msgid "No result found" msgstr "Pas de résultats" -#: forum/templates/forum/topic.jinja:23 +#: forum/templates/forum/topic.jinja msgid "Unmark as favorite" msgstr "Enlever des favoris" -#: forum/templates/forum/topic.jinja:25 +#: forum/templates/forum/topic.jinja msgid "Mark as favorite" msgstr "Ajouter aux favoris" -#: forum/views.py:201 +#: forum/views.py msgid "Apply rights and club owner recursively" msgstr "Appliquer les droits et le club propriétaire récursivement" -#: forum/views.py:431 +#: forum/views.py #, python-format msgid "%(author)s said" msgstr "Citation de %(author)s" -#: galaxy/models.py:56 +#: galaxy/models.py msgid "star owner" msgstr "propriétaire de l'étoile" -#: galaxy/models.py:61 +#: galaxy/models.py msgid "star mass" msgstr "masse de l'étoile" -#: galaxy/models.py:66 +#: galaxy/models.py msgid "the galaxy this star belongs to" msgstr "la galaxie à laquelle cette étoile appartient" -#: galaxy/models.py:102 +#: galaxy/models.py msgid "galaxy star 1" msgstr "étoile 1" -#: galaxy/models.py:108 +#: galaxy/models.py msgid "galaxy star 2" msgstr "étoile 2" -#: galaxy/models.py:113 +#: galaxy/models.py msgid "distance" msgstr "distance" -#: galaxy/models.py:115 +#: galaxy/models.py msgid "Distance separating star1 and star2" msgstr "Distance séparant étoile 1 et étoile 2" -#: galaxy/models.py:118 +#: galaxy/models.py msgid "family score" msgstr "score de famille" -#: galaxy/models.py:122 +#: galaxy/models.py msgid "pictures score" msgstr "score de photos" -#: galaxy/models.py:126 +#: galaxy/models.py msgid "clubs score" msgstr "score de club" -#: galaxy/models.py:181 +#: galaxy/models.py msgid "The galaxy current state" msgstr "L'état actuel de la galaxie" -#: galaxy/templates/galaxy/user.jinja:4 +#: galaxy/templates/galaxy/user.jinja #, python-format msgid "%(user_name)s's Galaxy" msgstr "Galaxie de %(user_name)s" -#: galaxy/views.py:48 +#: galaxy/views.py msgid "This citizen has not yet joined the galaxy" msgstr "Ce citoyen n'a pas encore rejoint la galaxie" -#: launderette/models.py:84 launderette/models.py:120 +#: launderette/models.py msgid "launderette" msgstr "laverie" -#: launderette/models.py:90 +#: launderette/models.py msgid "is working" msgstr "fonctionne" -#: launderette/models.py:93 +#: launderette/models.py msgid "Machine" msgstr "Machine" -#: launderette/models.py:126 +#: launderette/models.py msgid "borrow date" msgstr "date d'emprunt" -#: launderette/models.py:137 +#: launderette/models.py msgid "Token" msgstr "Jeton" -#: launderette/models.py:149 launderette/views.py:260 +#: launderette/models.py launderette/views.py msgid "Token name can not be blank" msgstr "Le nom du jeton ne peut pas être vide" -#: launderette/models.py:172 +#: launderette/models.py msgid "machine" msgstr "machine" -#: launderette/templates/launderette/launderette_admin.jinja:4 +#: launderette/templates/launderette/launderette_admin.jinja msgid "Launderette admin" msgstr "Gestion de la laverie" -#: launderette/templates/launderette/launderette_admin.jinja:9 +#: launderette/templates/launderette/launderette_admin.jinja msgid "Sell" msgstr "Vendre" -#: launderette/templates/launderette/launderette_admin.jinja:11 +#: launderette/templates/launderette/launderette_admin.jinja msgid "Machines" msgstr "Machines" -#: launderette/templates/launderette/launderette_admin.jinja:12 +#: launderette/templates/launderette/launderette_admin.jinja msgid "New machine" msgstr "Nouvelle machine" -#: launderette/templates/launderette/launderette_book.jinja:12 +#: launderette/templates/launderette/launderette_book.jinja msgid "Choose" msgstr "Choisir" -#: launderette/templates/launderette/launderette_book.jinja:23 +#: launderette/templates/launderette/launderette_book.jinja msgid "Washing and drying" msgstr "Lavage et séchage" -#: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:651 +#: launderette/templates/launderette/launderette_book.jinja sith/settings.py msgid "Washing" msgstr "Lavage" -#: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:651 +#: launderette/templates/launderette/launderette_book.jinja sith/settings.py msgid "Drying" msgstr "Séchage" -#: launderette/templates/launderette/launderette_list.jinja:4 -#: launderette/templates/launderette/launderette_list.jinja:12 +#: launderette/templates/launderette/launderette_list.jinja msgid "Launderette admin list" msgstr "Liste des laveries" -#: launderette/templates/launderette/launderette_list.jinja:9 +#: launderette/templates/launderette/launderette_list.jinja msgid "New launderette" msgstr "Nouvelle laverie" -#: launderette/templates/launderette/launderette_list.jinja:20 +#: launderette/templates/launderette/launderette_list.jinja msgid "There is no launderette in this website." msgstr "Il n'y a pas de laverie dans ce site web." -#: launderette/templates/launderette/launderette_main.jinja:9 +#: launderette/templates/launderette/launderette_main.jinja msgid "Edit presentation page" msgstr "Éditer la page de présentation" -#: launderette/templates/launderette/launderette_main.jinja:12 +#: launderette/templates/launderette/launderette_main.jinja msgid "Book launderette slot" msgstr "Réserver un créneau de laverie" -#: launderette/views.py:222 +#: launderette/views.py msgid "Tokens, separated by spaces" msgstr "Jetons, séparés par des espaces" -#: launderette/views.py:244 +#: launderette/views.py #, python-format msgid "Token %(token_name)s does not exists" msgstr "Le jeton %(token_name)s n'existe pas" -#: launderette/views.py:256 +#: launderette/views.py #, python-format msgid "Token %(token_name)s already exists" msgstr "Un jeton %(token_name)s existe déjà" -#: launderette/views.py:307 +#: launderette/views.py msgid "User has booked no slot" msgstr "L'utilisateur n'a pas réservé de créneau" -#: launderette/views.py:415 +#: launderette/views.py msgid "Token not found" msgstr "Jeton non trouvé" -#: matmat/templates/matmat/search_form.jinja:5 -#: matmat/templates/matmat/search_form.jinja:26 +#: matmat/templates/matmat/search_form.jinja msgid "Search user" msgstr "Rechercher un utilisateur" -#: matmat/templates/matmat/search_form.jinja:10 +#: matmat/templates/matmat/search_form.jinja msgid "Results" msgstr "Résultats" -#: matmat/templates/matmat/search_form.jinja:27 +#: matmat/templates/matmat/search_form.jinja msgid "Search by profile" msgstr "Recherche par profil" -#: matmat/templates/matmat/search_form.jinja:41 +#: matmat/templates/matmat/search_form.jinja msgid "Inverted search" msgstr "Recherche inversée" -#: matmat/templates/matmat/search_form.jinja:51 +#: matmat/templates/matmat/search_form.jinja msgid "Quick search" msgstr "Recherche rapide" -#: matmat/views.py:70 +#: matmat/views.py msgid "Last/First name or nickname" msgstr "Nom de famille, prénom ou surnom" -#: pedagogy/forms.py:81 +#: pedagogy/forms.py msgid "Do not vote" msgstr "Ne pas voter" -#: pedagogy/forms.py:128 +#: pedagogy/forms.py msgid "This user has already commented on this UV" msgstr "Cet utilisateur a déjà commenté cette UV" -#: pedagogy/forms.py:160 +#: pedagogy/forms.py msgid "Accepted reports" msgstr "Signalements acceptés" -#: pedagogy/forms.py:167 +#: pedagogy/forms.py msgid "Denied reports" msgstr "Signalements refusés" -#: pedagogy/models.py:48 +#: pedagogy/models.py msgid "" "The code of an UV must only contains uppercase characters without accent and " "numbers" @@ -5049,223 +4759,218 @@ msgstr "" "Le code d'une UV doit seulement contenir des caractères majuscule sans " "accents et nombres" -#: pedagogy/models.py:63 +#: pedagogy/models.py msgid "credit type" msgstr "type de crédit" -#: pedagogy/models.py:68 pedagogy/models.py:98 +#: pedagogy/models.py msgid "uv manager" msgstr "gestionnaire d'uv" -#: pedagogy/models.py:76 +#: pedagogy/models.py msgid "language" msgstr "langue" -#: pedagogy/models.py:82 +#: pedagogy/models.py msgid "credits" msgstr "crédits" -#: pedagogy/models.py:90 +#: pedagogy/models.py msgid "departmenmt" msgstr "département" -#: pedagogy/models.py:99 +#: pedagogy/models.py msgid "objectives" msgstr "objectifs" -#: pedagogy/models.py:100 +#: pedagogy/models.py msgid "program" msgstr "programme" -#: pedagogy/models.py:101 +#: pedagogy/models.py msgid "skills" msgstr "compétences" -#: pedagogy/models.py:102 +#: pedagogy/models.py msgid "key concepts" msgstr "concepts clefs" -#: pedagogy/models.py:107 +#: pedagogy/models.py msgid "hours CM" msgstr "heures CM" -#: pedagogy/models.py:114 +#: pedagogy/models.py msgid "hours TD" msgstr "heures TD" -#: pedagogy/models.py:121 +#: pedagogy/models.py msgid "hours TP" msgstr "heures TP" -#: pedagogy/models.py:128 +#: pedagogy/models.py msgid "hours THE" msgstr "heures THE" -#: pedagogy/models.py:135 +#: pedagogy/models.py msgid "hours TE" msgstr "heures TE" -#: pedagogy/models.py:206 pedagogy/models.py:282 +#: pedagogy/models.py msgid "uv" msgstr "UE" -#: pedagogy/models.py:210 +#: pedagogy/models.py msgid "global grade" msgstr "note globale" -#: pedagogy/models.py:217 +#: pedagogy/models.py msgid "utility grade" msgstr "note d'utilité" -#: pedagogy/models.py:224 +#: pedagogy/models.py msgid "interest grade" msgstr "note d'intérêt" -#: pedagogy/models.py:231 +#: pedagogy/models.py msgid "teaching grade" msgstr "note d'enseignement" -#: pedagogy/models.py:238 +#: pedagogy/models.py msgid "work load grade" msgstr "note de charge de travail" -#: pedagogy/models.py:244 +#: pedagogy/models.py msgid "publish date" msgstr "date de publication" -#: pedagogy/models.py:288 +#: pedagogy/models.py msgid "grade" msgstr "note" -#: pedagogy/models.py:309 +#: pedagogy/models.py msgid "report" msgstr "signaler" -#: pedagogy/models.py:315 +#: pedagogy/models.py msgid "reporter" msgstr "signalant" -#: pedagogy/models.py:318 +#: pedagogy/models.py msgid "reason" msgstr "raison" -#: pedagogy/templates/pedagogy/guide.jinja:5 +#: pedagogy/templates/pedagogy/guide.jinja msgid "UV Guide" msgstr "Guide des UVs" -#: pedagogy/templates/pedagogy/guide.jinja:59 +#: pedagogy/templates/pedagogy/guide.jinja #, python-format msgid "%(display_name)s" msgstr "%(display_name)s" -#: pedagogy/templates/pedagogy/guide.jinja:73 +#: pedagogy/templates/pedagogy/guide.jinja #, python-format msgid "%(credit_type)s" msgstr "%(credit_type)s" -#: pedagogy/templates/pedagogy/guide.jinja:91 -#: pedagogy/templates/pedagogy/moderation.jinja:12 +#: pedagogy/templates/pedagogy/guide.jinja +#: pedagogy/templates/pedagogy/moderation.jinja msgid "UV" msgstr "UE" -#: pedagogy/templates/pedagogy/guide.jinja:93 +#: pedagogy/templates/pedagogy/guide.jinja msgid "Department" msgstr "Département" -#: pedagogy/templates/pedagogy/guide.jinja:94 +#: pedagogy/templates/pedagogy/guide.jinja msgid "Credit type" msgstr "Type de crédit" -#: pedagogy/templates/pedagogy/macros.jinja:13 +#: pedagogy/templates/pedagogy/macros.jinja msgid " not rated " msgstr "non noté" -#: pedagogy/templates/pedagogy/moderation.jinja:4 +#: pedagogy/templates/pedagogy/moderation.jinja msgid "UV comment moderation" msgstr "Modération des commentaires d'UV" -#: pedagogy/templates/pedagogy/moderation.jinja:14 sas/models.py:308 +#: pedagogy/templates/pedagogy/moderation.jinja sas/models.py msgid "Reason" msgstr "Raison" -#: pedagogy/templates/pedagogy/moderation.jinja:29 +#: pedagogy/templates/pedagogy/moderation.jinja msgid "Delete comment" msgstr "Supprimer commentaire" -#: pedagogy/templates/pedagogy/moderation.jinja:30 +#: pedagogy/templates/pedagogy/moderation.jinja msgid "Delete report" msgstr "Supprimer signalement" -#: pedagogy/templates/pedagogy/uv_detail.jinja:10 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "UV Details" msgstr "Détails d'UV" -#: pedagogy/templates/pedagogy/uv_detail.jinja:31 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "CM: " msgstr "CM : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:34 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "TD: " msgstr "TD : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:37 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "TP: " msgstr "TP : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:40 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "TE: " msgstr "TE : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:43 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "THE: " msgstr "THE : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:61 -#: pedagogy/templates/pedagogy/uv_detail.jinja:156 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Global grade" msgstr "Note globale" -#: pedagogy/templates/pedagogy/uv_detail.jinja:62 -#: pedagogy/templates/pedagogy/uv_detail.jinja:157 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Utility" msgstr "Utilité" -#: pedagogy/templates/pedagogy/uv_detail.jinja:63 -#: pedagogy/templates/pedagogy/uv_detail.jinja:158 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Interest" msgstr "Intérêt" -#: pedagogy/templates/pedagogy/uv_detail.jinja:64 -#: pedagogy/templates/pedagogy/uv_detail.jinja:159 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Teaching" msgstr "Enseignement" -#: pedagogy/templates/pedagogy/uv_detail.jinja:65 -#: pedagogy/templates/pedagogy/uv_detail.jinja:160 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Work load" msgstr "Charge de travail" -#: pedagogy/templates/pedagogy/uv_detail.jinja:75 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Objectives" msgstr "Objectifs" -#: pedagogy/templates/pedagogy/uv_detail.jinja:77 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Program" msgstr "Programme" -#: pedagogy/templates/pedagogy/uv_detail.jinja:79 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Earned skills" msgstr "Compétences acquises" -#: pedagogy/templates/pedagogy/uv_detail.jinja:81 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Key concepts" msgstr "Concepts clefs" -#: pedagogy/templates/pedagogy/uv_detail.jinja:83 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "UE manager: " msgstr "Gestionnaire d'UE : " -#: pedagogy/templates/pedagogy/uv_detail.jinja:90 pedagogy/tests/tests.py:384 +#: pedagogy/templates/pedagogy/uv_detail.jinja pedagogy/tests/tests.py msgid "" "You already posted a comment on this UV. If you want to comment again, " "please modify or delete your previous comment." @@ -5273,53 +4978,52 @@ msgstr "" "Vous avez déjà commenté cette UV. Si vous voulez de nouveau commenter, " "veuillez modifier ou supprimer votre commentaire précédent." -#: pedagogy/templates/pedagogy/uv_detail.jinja:94 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Leave comment" msgstr "Laisser un commentaire" -#: pedagogy/templates/pedagogy/uv_detail.jinja:150 -#: trombi/templates/trombi/export.jinja:70 +#: pedagogy/templates/pedagogy/uv_detail.jinja +#: trombi/templates/trombi/export.jinja msgid "Comments" msgstr "Commentaires" -#: pedagogy/templates/pedagogy/uv_detail.jinja:182 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "This comment has been reported" msgstr "Ce commentaire a été signalé" -#: pedagogy/templates/pedagogy/uv_detail.jinja:195 +#: pedagogy/templates/pedagogy/uv_detail.jinja msgid "Report this comment" msgstr "Signaler ce commentaire" -#: pedagogy/templates/pedagogy/uv_edit.jinja:4 -#: pedagogy/templates/pedagogy/uv_edit.jinja:8 +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "Edit UE" msgstr "Éditer l'UE" -#: pedagogy/templates/pedagogy/uv_edit.jinja:27 +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "Import from UTBM" msgstr "Importer depuis l'UTBM" -#: pedagogy/templates/pedagogy/uv_edit.jinja:62 +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "Unknown UE code" msgstr "Code d'UE inconnu" -#: pedagogy/templates/pedagogy/uv_edit.jinja:79 +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "Successful autocomplete" msgstr "Autocomplétion réussite" -#: pedagogy/templates/pedagogy/uv_edit.jinja:82 +#: pedagogy/templates/pedagogy/uv_edit.jinja msgid "An error occurred: " msgstr "Une erreur est survenue : " -#: rootplace/templates/rootplace/delete_user_messages.jinja:8 +#: rootplace/templates/rootplace/delete_user_messages.jinja msgid "Delete all forum messages from an user" msgstr "Supprimer tous les messages forum d'un utilisateur" -#: rootplace/templates/rootplace/delete_user_messages.jinja:12 +#: rootplace/templates/rootplace/delete_user_messages.jinja msgid "Delete messages" msgstr "Supprimer les messages" -#: rootplace/templates/rootplace/delete_user_messages.jinja:14 +#: rootplace/templates/rootplace/delete_user_messages.jinja msgid "" "If you have trouble using this utility (timeout error, 500 error), try using " "the command line utility. Use ./manage.py delete_all_forum_user_messages ID." @@ -5328,114 +5032,113 @@ msgstr "" "erreur 500), essayez en utilisant l'utilitaire en ligne de commande. " "Utilisez ./manage.py delete_user_messages ID." -#: rootplace/templates/rootplace/logs.jinja:15 +#: rootplace/templates/rootplace/logs.jinja msgid "Operator" msgstr "Opérateur" -#: rootplace/templates/rootplace/merge.jinja:8 +#: rootplace/templates/rootplace/merge.jinja msgid "Merge two users" msgstr "Fusionner deux utilisateurs" -#: rootplace/templates/rootplace/merge.jinja:12 +#: rootplace/templates/rootplace/merge.jinja msgid "Merge" msgstr "Fusion" -#: rootplace/views.py:160 +#: rootplace/views.py msgid "User that will be kept" msgstr "Utilisateur qui sera conservé" -#: rootplace/views.py:167 +#: rootplace/views.py msgid "User that will be deleted" msgstr "Utilisateur qui sera supprimé" -#: rootplace/views.py:177 +#: rootplace/views.py msgid "User to be selected" msgstr "Utilisateur à sélectionner" -#: sas/forms.py:16 +#: sas/forms.py msgid "Add a new album" msgstr "Ajouter un nouvel album" -#: sas/forms.py:19 +#: sas/forms.py msgid "Upload images" msgstr "Envoyer les images" -#: sas/forms.py:37 +#: sas/forms.py #, python-format msgid "Error creating album %(album)s: %(msg)s" msgstr "Erreur de création de l'album %(album)s : %(msg)s" -#: sas/forms.py:107 +#: sas/forms.py msgid "You already requested moderation for this picture." msgstr "Vous avez déjà déposé une demande de retrait pour cette photo." -#: sas/models.py:279 +#: sas/models.py msgid "picture" msgstr "photo" -#: sas/models.py:303 +#: sas/models.py msgid "Picture" msgstr "Photo" -#: sas/models.py:310 +#: sas/models.py msgid "Why do you want this image to be removed ?" msgstr "Pourquoi voulez-vous retirer cette image ?" -#: sas/models.py:314 +#: sas/models.py msgid "Picture moderation request" msgstr "Demande de modération de photo" -#: sas/models.py:315 +#: sas/models.py msgid "Picture moderation requests" msgstr "Demandes de modération de photo" -#: sas/templates/sas/album.jinja:13 -#: sas/templates/sas/ask_picture_removal.jinja:4 sas/templates/sas/main.jinja:8 -#: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:15 +#: sas/templates/sas/album.jinja sas/templates/sas/ask_picture_removal.jinja +#: sas/templates/sas/main.jinja sas/templates/sas/picture.jinja msgid "SAS" msgstr "SAS" -#: sas/templates/sas/album.jinja:56 sas/templates/sas/moderation.jinja:10 +#: sas/templates/sas/album.jinja sas/templates/sas/moderation.jinja msgid "Albums" msgstr "Albums" -#: sas/templates/sas/album.jinja:100 +#: sas/templates/sas/album.jinja msgid "Upload" msgstr "Envoyer" -#: sas/templates/sas/album.jinja:107 +#: sas/templates/sas/album.jinja msgid "Template generation time: " msgstr "Temps de génération du template : " -#: sas/templates/sas/ask_picture_removal.jinja:9 +#: sas/templates/sas/ask_picture_removal.jinja msgid "Image removal request" msgstr "Demande de retrait d'image" -#: sas/templates/sas/ask_picture_removal.jinja:25 +#: sas/templates/sas/ask_picture_removal.jinja msgid "Request removal" msgstr "Demander le retrait" -#: sas/templates/sas/main.jinja:20 +#: sas/templates/sas/main.jinja msgid "You must be logged in to see the SAS." msgstr "Vous devez être connecté pour voir les photos." -#: sas/templates/sas/main.jinja:23 +#: sas/templates/sas/main.jinja msgid "Latest albums" msgstr "Derniers albums" -#: sas/templates/sas/main.jinja:38 sas/templates/sas/main.jinja:53 +#: sas/templates/sas/main.jinja msgid "All categories" msgstr "Toutes les catégories" -#: sas/templates/sas/moderation.jinja:4 sas/templates/sas/moderation.jinja:8 +#: sas/templates/sas/moderation.jinja msgid "SAS moderation" msgstr "Modération du SAS" -#: sas/templates/sas/picture.jinja:38 +#: sas/templates/sas/picture.jinja msgid "Asked for removal" msgstr "Retrait demandé" -#: sas/templates/sas/picture.jinja:41 +#: sas/templates/sas/picture.jinja msgid "" "This picture can be viewed only by root users and by SAS admins. It will be " "hidden to other users until it has been moderated." @@ -5444,437 +5147,437 @@ msgstr "" "SAS. Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas " "modérée." -#: sas/templates/sas/picture.jinja:49 +#: sas/templates/sas/picture.jinja msgid "The following issues have been raised:" msgstr "Les problèmes suivants ont été remontés :" -#: sas/templates/sas/picture.jinja:114 +#: sas/templates/sas/picture.jinja msgid "HD version" msgstr "Version HD" -#: sas/templates/sas/picture.jinja:118 +#: sas/templates/sas/picture.jinja msgid "Ask for removal" msgstr "Demander le retrait" -#: sas/templates/sas/picture.jinja:139 sas/templates/sas/picture.jinja:150 +#: sas/templates/sas/picture.jinja msgid "Previous picture" msgstr "Image précédente" -#: sas/templates/sas/picture.jinja:158 +#: sas/templates/sas/picture.jinja msgid "People" msgstr "Personne(s)" -#: sas/templates/sas/picture.jinja:165 +#: sas/templates/sas/picture.jinja msgid "Identify users on pictures" msgstr "Identifiez les utilisateurs sur les photos" -#: sith/settings.py:253 sith/settings.py:470 +#: sith/settings.py msgid "English" msgstr "Anglais" -#: sith/settings.py:253 sith/settings.py:469 +#: sith/settings.py msgid "French" msgstr "Français" -#: sith/settings.py:397 +#: sith/settings.py msgid "TC" msgstr "TC" -#: sith/settings.py:398 +#: sith/settings.py msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:399 +#: sith/settings.py msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:400 +#: sith/settings.py msgid "INFO" msgstr "INFO" -#: sith/settings.py:401 +#: sith/settings.py msgid "GI" msgstr "GI" -#: sith/settings.py:402 sith/settings.py:480 +#: sith/settings.py msgid "E" msgstr "E" -#: sith/settings.py:403 +#: sith/settings.py msgid "EE" msgstr "EE" -#: sith/settings.py:404 +#: sith/settings.py msgid "GESC" msgstr "GESC" -#: sith/settings.py:405 +#: sith/settings.py msgid "GMC" msgstr "GMC" -#: sith/settings.py:406 +#: sith/settings.py msgid "MC" msgstr "MC" -#: sith/settings.py:407 +#: sith/settings.py msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:408 +#: sith/settings.py msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:409 +#: sith/settings.py msgid "N/A" msgstr "N/A" -#: sith/settings.py:415 +#: sith/settings.py msgid "Transfert" msgstr "Virement" -#: sith/settings.py:428 +#: sith/settings.py msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:429 +#: sith/settings.py msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:430 +#: sith/settings.py msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:450 +#: sith/settings.py msgid "Free" msgstr "Libre" -#: sith/settings.py:451 +#: sith/settings.py msgid "CS" msgstr "CS" -#: sith/settings.py:452 +#: sith/settings.py msgid "TM" msgstr "TM" -#: sith/settings.py:453 +#: sith/settings.py msgid "OM" msgstr "OM" -#: sith/settings.py:454 +#: sith/settings.py msgid "QC" msgstr "QC" -#: sith/settings.py:455 +#: sith/settings.py msgid "EC" msgstr "EC" -#: sith/settings.py:456 +#: sith/settings.py msgid "RN" msgstr "RN" -#: sith/settings.py:457 +#: sith/settings.py msgid "ST" msgstr "ST" -#: sith/settings.py:458 +#: sith/settings.py msgid "EXT" msgstr "EXT" -#: sith/settings.py:463 +#: sith/settings.py msgid "Autumn" msgstr "Automne" -#: sith/settings.py:464 +#: sith/settings.py msgid "Spring" msgstr "Printemps" -#: sith/settings.py:465 +#: sith/settings.py msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:471 +#: sith/settings.py msgid "German" msgstr "Allemand" -#: sith/settings.py:472 +#: sith/settings.py msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:476 +#: sith/settings.py msgid "A" msgstr "A" -#: sith/settings.py:477 +#: sith/settings.py msgid "B" msgstr "B" -#: sith/settings.py:478 +#: sith/settings.py msgid "C" msgstr "C" -#: sith/settings.py:479 +#: sith/settings.py msgid "D" msgstr "D" -#: sith/settings.py:481 +#: sith/settings.py msgid "FX" msgstr "FX" -#: sith/settings.py:482 +#: sith/settings.py msgid "F" msgstr "F" -#: sith/settings.py:483 +#: sith/settings.py msgid "Abs" msgstr "Abs" -#: sith/settings.py:487 +#: sith/settings.py msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:488 +#: sith/settings.py msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:532 +#: sith/settings.py msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:533 +#: sith/settings.py msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:535 +#: sith/settings.py msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:539 +#: sith/settings.py msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:540 +#: sith/settings.py msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:541 +#: sith/settings.py msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:542 +#: sith/settings.py msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:543 +#: sith/settings.py msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:544 +#: sith/settings.py msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:545 +#: sith/settings.py msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:546 +#: sith/settings.py msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:548 +#: sith/settings.py msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:552 +#: sith/settings.py msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:553 +#: sith/settings.py msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:554 +#: sith/settings.py msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:556 +#: sith/settings.py msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:560 +#: sith/settings.py msgid "One day" msgstr "Un jour" -#: sith/settings.py:561 +#: sith/settings.py msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:564 +#: sith/settings.py msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:569 +#: sith/settings.py msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:574 +#: sith/settings.py msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:579 +#: sith/settings.py msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:584 +#: sith/settings.py msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:590 +#: sith/settings.py msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:610 +#: sith/settings.py msgid "President" msgstr "Président⸱e" -#: sith/settings.py:611 +#: sith/settings.py msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:612 +#: sith/settings.py msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:613 +#: sith/settings.py msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:614 +#: sith/settings.py msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:615 +#: sith/settings.py msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:616 +#: sith/settings.py msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:617 +#: sith/settings.py msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:618 +#: sith/settings.py msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:655 +#: sith/settings.py msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:656 +#: sith/settings.py msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:659 +#: sith/settings.py msgid "A new pedagogy comment has been signaled for moderation" msgstr "" "Un nouveau commentaire de la pédagogie a été signalé pour la modération" -#: sith/settings.py:661 +#: sith/settings.py #, python-format msgid "There are %s fresh news to be moderated" msgstr "Il y a %s nouvelles toutes fraîches à modérer" -#: sith/settings.py:662 +#: sith/settings.py msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:663 +#: sith/settings.py #, python-format msgid "There are %s pictures to be moderated in the SAS" msgstr "Il y a %s photos à modérer dans le SAS" -#: sith/settings.py:664 +#: sith/settings.py msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:665 +#: sith/settings.py #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:666 +#: sith/settings.py #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:667 +#: sith/settings.py msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:679 +#: sith/settings.py msgid "Success!" msgstr "Succès !" -#: sith/settings.py:680 +#: sith/settings.py msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:681 +#: sith/settings.py msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:682 +#: sith/settings.py msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:683 +#: sith/settings.py msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:691 +#: sith/settings.py msgid "AE tee-shirt" msgstr "Tee-shirt AE" -#: subscription/forms.py:93 +#: subscription/forms.py msgid "A user with that email address already exists" msgstr "Un utilisateur avec cette adresse email existe déjà" -#: subscription/models.py:34 +#: subscription/models.py msgid "Bad subscription type" msgstr "Mauvais type de cotisation" -#: subscription/models.py:39 +#: subscription/models.py msgid "Bad payment method" msgstr "Mauvais type de paiement" -#: subscription/models.py:47 +#: subscription/models.py msgid "subscription type" msgstr "type d'inscription" -#: subscription/models.py:53 +#: subscription/models.py msgid "subscription start" msgstr "début de la cotisation" -#: subscription/models.py:54 +#: subscription/models.py msgid "subscription end" msgstr "fin de la cotisation" -#: subscription/models.py:63 +#: subscription/models.py msgid "location" msgstr "lieu" -#: subscription/models.py:107 +#: subscription/models.py msgid "You can not subscribe many time for the same period" msgstr "Vous ne pouvez pas cotiser plusieurs fois pour la même période" -#: subscription/templates/subscription/fragments/creation_success.jinja:4 +#: subscription/templates/subscription/fragments/creation_success.jinja #, python-format msgid "Subscription created for %(user)s" msgstr "Cotisation créée pour %(user)s" -#: subscription/templates/subscription/fragments/creation_success.jinja:7 +#: subscription/templates/subscription/fragments/creation_success.jinja #, python-format msgid "" "%(user)s received its new %(type)s subscription. It will be active until " @@ -5883,31 +5586,31 @@ msgstr "" "%(user)s a reçu sa nouvelle cotisaton %(type)s. Elle sert active jusqu'au " "%(end)s inclu." -#: subscription/templates/subscription/fragments/creation_success.jinja:15 +#: subscription/templates/subscription/fragments/creation_success.jinja msgid "Go to user profile" msgstr "Voir le profil de l'utilisateur" -#: subscription/templates/subscription/fragments/creation_success.jinja:23 +#: subscription/templates/subscription/fragments/creation_success.jinja msgid "Create another subscription" msgstr "Créer une nouvelle cotisation" -#: subscription/templates/subscription/stats.jinja:27 +#: subscription/templates/subscription/stats.jinja msgid "Total subscriptions" msgstr "Cotisations totales" -#: subscription/templates/subscription/stats.jinja:28 +#: subscription/templates/subscription/stats.jinja msgid "Subscriptions by type" msgstr "Cotisations par type" -#: subscription/templates/subscription/subscription.jinja:38 +#: subscription/templates/subscription/subscription.jinja msgid "Existing member" msgstr "Membre existant" -#: trombi/models.py:55 +#: trombi/models.py msgid "subscription deadline" msgstr "fin des inscriptions" -#: trombi/models.py:58 +#: trombi/models.py msgid "" "Before this date, users are allowed to subscribe to this Trombi. After this " "date, users subscribed will be allowed to comment on each other." @@ -5916,46 +5619,46 @@ msgstr "" "Après cette date, les utilisateurs inscrits peuvent se soumettre des " "commentaires entre eux." -#: trombi/models.py:64 +#: trombi/models.py msgid "comments deadline" msgstr "fin des commentaires" -#: trombi/models.py:67 +#: trombi/models.py msgid "After this date, users won't be able to make comments anymore." msgstr "" "Après cette date, les utilisateurs ne peuvent plus faire de commentaires." -#: trombi/models.py:71 +#: trombi/models.py msgid "maximum characters" msgstr "nombre de caractères max" -#: trombi/models.py:73 +#: trombi/models.py msgid "Maximum number of characters allowed in a comment." msgstr "Nombre maximum de caractères autorisés dans un commentaire." -#: trombi/models.py:76 +#: trombi/models.py msgid "show users profiles to each other" msgstr "montrer les profils aux inscrits" -#: trombi/models.py:93 +#: trombi/models.py msgid "" "Closing the subscriptions after the comments is definitively not a good idea." msgstr "" "Fermer les inscriptions après les commentaires est vraiment une idée pourrie." -#: trombi/models.py:116 +#: trombi/models.py msgid "trombi user" msgstr "utilisateur trombi" -#: trombi/models.py:122 +#: trombi/models.py msgid "trombi" msgstr "trombi" -#: trombi/models.py:130 +#: trombi/models.py msgid "profile pict" msgstr "photo de profil" -#: trombi/models.py:134 +#: trombi/models.py msgid "" "The profile picture you want in the trombi (warning: this picture may be " "published)" @@ -5963,11 +5666,11 @@ msgstr "" "La photo de profil que vous souhaitez voir dans le Trombi (attention: cette " "photo risque d'être publiée)" -#: trombi/models.py:140 +#: trombi/models.py msgid "scrub pict" msgstr "photo de blouse" -#: trombi/models.py:144 +#: trombi/models.py msgid "" "The scrub picture you want in the trombi (warning: this picture may be " "published)" @@ -5975,74 +5678,69 @@ msgstr "" "La photo de blouse que vous souhaitez voir dans le Trombi (attention: cette " "photo risque d'être publiée)" -#: trombi/models.py:184 +#: trombi/models.py msgid "target" msgstr "cible" -#: trombi/models.py:189 +#: trombi/models.py msgid "is the comment moderated" msgstr "le commentaire est modéré" -#: trombi/models.py:211 +#: trombi/models.py msgid "start" msgstr "début" -#: trombi/models.py:212 +#: trombi/models.py msgid "end" msgstr "fin" -#: trombi/templates/trombi/comment_moderation.jinja:4 -#: trombi/templates/trombi/comment_moderation.jinja:8 +#: trombi/templates/trombi/comment_moderation.jinja msgid "Moderate Trombi comments" msgstr "Modérer les commentaires du Trombi" -#: trombi/templates/trombi/comment_moderation.jinja:23 +#: trombi/templates/trombi/comment_moderation.jinja msgid "Accept" msgstr "Accepter" -#: trombi/templates/trombi/comment_moderation.jinja:28 +#: trombi/templates/trombi/comment_moderation.jinja msgid "Reject" msgstr "Refuser" -#: trombi/templates/trombi/detail.jinja:4 -#: trombi/templates/trombi/detail.jinja:8 -#: trombi/templates/trombi/export.jinja:4 -#: trombi/templates/trombi/export.jinja:8 +#: trombi/templates/trombi/detail.jinja trombi/templates/trombi/export.jinja #, python-format msgid "%(club)s's Trombi" msgstr "Trombi de %(club)s" -#: trombi/templates/trombi/detail.jinja:11 +#: trombi/templates/trombi/detail.jinja msgid "Subscription deadline: " msgstr "Fin des inscriptions : " -#: trombi/templates/trombi/detail.jinja:12 +#: trombi/templates/trombi/detail.jinja msgid "Comment deadline: " msgstr "Fin des commentaires : " -#: trombi/templates/trombi/detail.jinja:13 +#: trombi/templates/trombi/detail.jinja msgid "Export" msgstr "Exporter" -#: trombi/templates/trombi/detail.jinja:15 +#: trombi/templates/trombi/detail.jinja msgid "Add user" msgstr "Ajouter une personne" -#: trombi/templates/trombi/detail.jinja:36 +#: trombi/templates/trombi/detail.jinja msgid "Add club membership" msgstr "Ajouter appartenance à un club" -#: trombi/templates/trombi/edit_profile.jinja:4 -#: trombi/templates/trombi/edit_profile.jinja:8 +#: trombi/templates/trombi/edit_profile.jinja msgid "Edit profile" msgstr "Éditer mon profil" -#: trombi/templates/trombi/edit_profile.jinja:9 -#: trombi/templates/trombi/user_profile.jinja:9 +#: trombi/templates/trombi/edit_profile.jinja +#: trombi/templates/trombi/user_profile.jinja msgid "Back to tools" msgstr "Retour aux outils" -#: trombi/templates/trombi/edit_profile.jinja:17 +#: trombi/templates/trombi/edit_profile.jinja msgid "" "Reset club memberships in Trombi (delete exising ones, does not impact real " "club memberships)" @@ -6050,67 +5748,66 @@ msgstr "" "Réinitialiser les participations aux clubs dans le Trombi (supprime les " "existantes, n'impacte pas les vraies appartenances du site)" -#: trombi/templates/trombi/export.jinja:17 +#: trombi/templates/trombi/export.jinja msgid "Quote: " msgstr "Citation : " -#: trombi/templates/trombi/export.jinja:18 +#: trombi/templates/trombi/export.jinja msgid "Date of birth: " msgstr "Date de naissance : " -#: trombi/templates/trombi/export.jinja:19 +#: trombi/templates/trombi/export.jinja msgid "Email: " msgstr "Email : " -#: trombi/templates/trombi/export.jinja:21 +#: trombi/templates/trombi/export.jinja msgid "City: " msgstr "Ville : " -#: trombi/templates/trombi/export.jinja:23 +#: trombi/templates/trombi/export.jinja msgid "Copy" msgstr "Copier" -#: trombi/templates/trombi/export.jinja:42 +#: trombi/templates/trombi/export.jinja msgid "Copy profile picture" msgstr "Copier la photo de profil" -#: trombi/templates/trombi/export.jinja:46 +#: trombi/templates/trombi/export.jinja msgid "Copy scrub picture" msgstr "Copier la photo de blouse" -#: trombi/templates/trombi/export.jinja:68 +#: trombi/templates/trombi/export.jinja msgid "Copy clubs" msgstr "Copier les clubs" -#: trombi/templates/trombi/export.jinja:85 +#: trombi/templates/trombi/export.jinja msgid "Copy comments" msgstr "Copier les commentaires" -#: trombi/templates/trombi/user_profile.jinja:4 -#: trombi/templates/trombi/user_profile.jinja:8 +#: trombi/templates/trombi/user_profile.jinja #, python-format msgid "%(user_name)s's Trombi profile" msgstr "Profil Trombi de %(user_name)s" -#: trombi/templates/trombi/user_tools.jinja:4 +#: trombi/templates/trombi/user_tools.jinja #, python-format msgid "%(user_name)s's Trombi" msgstr "Trombi de %(user_name)s" -#: trombi/templates/trombi/user_tools.jinja:8 +#: trombi/templates/trombi/user_tools.jinja msgid "Trombi'" msgstr "Trombi'" -#: trombi/templates/trombi/user_tools.jinja:17 +#: trombi/templates/trombi/user_tools.jinja #, python-format msgid "You are subscribed to the Trombi %(trombi)s" msgstr "Vous êtes inscrit au Trombi %(trombi)s" -#: trombi/templates/trombi/user_tools.jinja:22 +#: trombi/templates/trombi/user_tools.jinja msgid "You can not write comments at this date." msgstr "Vous ne pouvez pas commenter à cette date." -#: trombi/templates/trombi/user_tools.jinja:24 +#: trombi/templates/trombi/user_tools.jinja #, python-format msgid "" "Comments are only allowed between %(start)s (excluded) and %(end)s (included)" @@ -6118,31 +5815,31 @@ msgstr "" "Les commentaires sont autorisés entre le %(start)s (exclu) et le %(end)s " "(inclu)" -#: trombi/templates/trombi/user_tools.jinja:49 +#: trombi/templates/trombi/user_tools.jinja msgid "Edit comment" msgstr "Éditer le commentaire" -#: trombi/views.py:69 +#: trombi/views.py msgid "My profile" msgstr "Mon profil" -#: trombi/views.py:76 +#: trombi/views.py msgid "My pictures" msgstr "Mes photos" -#: trombi/views.py:91 +#: trombi/views.py msgid "Admin tools" msgstr "Admin Trombi" -#: trombi/views.py:219 +#: trombi/views.py msgid "Explain why you rejected the comment" msgstr "Expliquez pourquoi vous refusez le commentaire" -#: trombi/views.py:250 +#: trombi/views.py msgid "Rejected comment" msgstr "Commentaire rejeté" -#: trombi/views.py:252 +#: trombi/views.py #, python-format msgid "" "Your comment to %(target)s on the Trombi \"%(trombi)s\" was rejected for the " @@ -6159,16 +5856,16 @@ msgstr "" "\n" "%(content)s" -#: trombi/views.py:284 +#: trombi/views.py #, python-format msgid "%(name)s (deadline: %(date)s)" msgstr "%(name)s (date limite: %(date)s)" -#: trombi/views.py:294 +#: trombi/views.py msgid "Select trombi" msgstr "Choisir un trombi" -#: trombi/views.py:296 +#: trombi/views.py msgid "" "This allows you to subscribe to a Trombi. Be aware that you can subscribe " "only once, so don't play with that, or you will expose yourself to the " @@ -6178,19 +5875,19 @@ msgstr "" "pouvez vous inscrire qu'à un seul Trombi, donc ne jouez pas avec cet option " "ou vous encourerez la colère des admins!" -#: trombi/views.py:367 +#: trombi/views.py msgid "Personal email (not UTBM)" msgstr "Email personnel (pas UTBM)" -#: trombi/views.py:368 +#: trombi/views.py msgid "Phone" msgstr "Téléphone" -#: trombi/views.py:369 +#: trombi/views.py msgid "Native town" msgstr "Ville d'origine" -#: trombi/views.py:477 +#: trombi/views.py msgid "" "You can not yet write comment, you must wait for the subscription deadline " "to be passed." @@ -6198,11 +5895,11 @@ msgstr "" "Vous ne pouvez pas encore écrire de commentaires, vous devez attendre la fin " "des inscriptions" -#: trombi/views.py:484 +#: trombi/views.py msgid "You can not write comment anymore, the deadline is already passed." msgstr "Vous ne pouvez plus écrire de commentaires, la date est passée." -#: trombi/views.py:497 +#: trombi/views.py #, python-format msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 7412eac5..4c7c5dec 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-23 02:38+0100\n" +"POT-Creation-Date: 2025-01-04 22:00+0100\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -17,168 +17,168 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: core/static/bundled/core/components/ajax-select-base.ts:68 +#: core/static/bundled/core/components/ajax-select-base.ts msgid "Remove" msgstr "Retirer" -#: core/static/bundled/core/components/ajax-select-base.ts:90 +#: core/static/bundled/core/components/ajax-select-base.ts msgid "You need to type %(number)s more characters" msgstr "Vous devez taper %(number)s caractères de plus" -#: core/static/bundled/core/components/ajax-select-base.ts:94 +#: core/static/bundled/core/components/ajax-select-base.ts msgid "No results found" msgstr "Aucun résultat trouvé" -#: core/static/bundled/core/components/easymde-index.ts:38 +#: core/static/bundled/core/components/easymde-index.ts msgid "Heading" msgstr "Titre" -#: core/static/bundled/core/components/easymde-index.ts:44 +#: core/static/bundled/core/components/easymde-index.ts msgid "Italic" msgstr "Italique" -#: core/static/bundled/core/components/easymde-index.ts:50 +#: core/static/bundled/core/components/easymde-index.ts msgid "Bold" msgstr "Gras" -#: core/static/bundled/core/components/easymde-index.ts:56 +#: core/static/bundled/core/components/easymde-index.ts msgid "Strikethrough" msgstr "Barré" -#: core/static/bundled/core/components/easymde-index.ts:65 +#: core/static/bundled/core/components/easymde-index.ts msgid "Underline" msgstr "Souligné" -#: core/static/bundled/core/components/easymde-index.ts:74 +#: core/static/bundled/core/components/easymde-index.ts msgid "Superscript" msgstr "Exposant" -#: core/static/bundled/core/components/easymde-index.ts:83 +#: core/static/bundled/core/components/easymde-index.ts msgid "Subscript" msgstr "Indice" -#: core/static/bundled/core/components/easymde-index.ts:89 +#: core/static/bundled/core/components/easymde-index.ts msgid "Code" msgstr "Code" -#: core/static/bundled/core/components/easymde-index.ts:96 +#: core/static/bundled/core/components/easymde-index.ts msgid "Quote" msgstr "Citation" -#: core/static/bundled/core/components/easymde-index.ts:102 +#: core/static/bundled/core/components/easymde-index.ts msgid "Unordered list" msgstr "Liste non ordonnée" -#: core/static/bundled/core/components/easymde-index.ts:108 +#: core/static/bundled/core/components/easymde-index.ts msgid "Ordered list" msgstr "Liste ordonnée" -#: core/static/bundled/core/components/easymde-index.ts:115 +#: core/static/bundled/core/components/easymde-index.ts msgid "Insert link" msgstr "Insérer lien" -#: core/static/bundled/core/components/easymde-index.ts:121 +#: core/static/bundled/core/components/easymde-index.ts msgid "Insert image" msgstr "Insérer image" -#: core/static/bundled/core/components/easymde-index.ts:127 +#: core/static/bundled/core/components/easymde-index.ts msgid "Insert table" msgstr "Insérer tableau" -#: core/static/bundled/core/components/easymde-index.ts:134 +#: core/static/bundled/core/components/easymde-index.ts msgid "Clean block" msgstr "Nettoyer bloc" -#: core/static/bundled/core/components/easymde-index.ts:141 +#: core/static/bundled/core/components/easymde-index.ts msgid "Toggle preview" msgstr "Activer la prévisualisation" -#: core/static/bundled/core/components/easymde-index.ts:147 +#: core/static/bundled/core/components/easymde-index.ts msgid "Toggle side by side" msgstr "Activer la vue côte à côte" -#: core/static/bundled/core/components/easymde-index.ts:153 +#: core/static/bundled/core/components/easymde-index.ts msgid "Toggle fullscreen" msgstr "Activer le plein écran" -#: core/static/bundled/core/components/easymde-index.ts:160 +#: core/static/bundled/core/components/easymde-index.ts msgid "Markdown guide" msgstr "Guide markdown" -#: core/static/bundled/core/components/nfc-input-index.ts:26 +#: core/static/bundled/core/components/nfc-input-index.ts msgid "Unsupported NFC card" msgstr "Carte NFC non supportée" -#: core/static/bundled/user/family-graph-index.js:233 +#: core/static/bundled/user/family-graph-index.js msgid "family_tree.%(extension)s" msgstr "arbre_genealogique.%(extension)s" -#: core/static/bundled/user/pictures-index.js:76 +#: core/static/bundled/user/pictures-index.js msgid "pictures.%(extension)s" msgstr "photos.%(extension)s" -#: core/static/user/js/user_edit.js:91 +#: core/static/user/js/user_edit.js #, javascript-format msgid "captured.%s" msgstr "capture.%s" -#: counter/static/bundled/counter/counter-click-index.ts:60 +#: counter/static/bundled/counter/counter-click-index.ts msgid "Not enough money" msgstr "Pas assez d'argent" -#: counter/static/bundled/counter/counter-click-index.ts:113 +#: counter/static/bundled/counter/counter-click-index.ts msgid "You can't send an empty basket." msgstr "Vous ne pouvez pas envoyer un panier vide." -#: counter/static/bundled/counter/product-list-index.ts:40 +#: counter/static/bundled/counter/product-list-index.ts msgid "name" msgstr "nom" -#: counter/static/bundled/counter/product-list-index.ts:43 +#: counter/static/bundled/counter/product-list-index.ts msgid "product type" msgstr "type de produit" -#: counter/static/bundled/counter/product-list-index.ts:45 +#: counter/static/bundled/counter/product-list-index.ts msgid "limit age" msgstr "limite d'âge" -#: counter/static/bundled/counter/product-list-index.ts:46 +#: counter/static/bundled/counter/product-list-index.ts msgid "purchase price" msgstr "prix d'achat" -#: counter/static/bundled/counter/product-list-index.ts:47 +#: counter/static/bundled/counter/product-list-index.ts msgid "selling price" msgstr "prix de vente" -#: counter/static/bundled/counter/product-list-index.ts:48 +#: counter/static/bundled/counter/product-list-index.ts msgid "archived" msgstr "archivé" -#: counter/static/bundled/counter/product-list-index.ts:125 +#: counter/static/bundled/counter/product-list-index.ts msgid "Uncategorized" msgstr "Sans catégorie" -#: counter/static/bundled/counter/product-list-index.ts:143 +#: counter/static/bundled/counter/product-list-index.ts msgid "products.csv" msgstr "produits.csv" -#: counter/static/bundled/counter/product-type-index.ts:46 +#: counter/static/bundled/counter/product-type-index.ts msgid "Products types reordered!" msgstr "Types de produits réordonnés !" -#: counter/static/bundled/counter/product-type-index.ts:50 +#: counter/static/bundled/counter/product-type-index.ts #, javascript-format msgid "Product type reorganisation failed with status code : %d" msgstr "La réorganisation des types de produit a échoué avec le code : %d" -#: eboutic/static/eboutic/js/makecommand.js:56 +#: eboutic/static/eboutic/js/makecommand.js msgid "Incorrect value" msgstr "Valeur incorrecte" -#: sas/static/bundled/sas/viewer-index.ts:271 +#: sas/static/bundled/sas/viewer-index.ts msgid "Couldn't moderate picture" msgstr "Il n'a pas été possible de modérer l'image" -#: sas/static/bundled/sas/viewer-index.ts:284 +#: sas/static/bundled/sas/viewer-index.ts msgid "Couldn't delete picture" msgstr "Il n'a pas été possible de supprimer l'image" From 63839dc22b55a742c34cf09d0d2d3fcaaad2ced1 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 28 Dec 2024 17:33:26 +0100 Subject: [PATCH 14/40] Fix poster edition and display bug --- com/templates/com/screen_slideshow.jinja | 2 +- com/views.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/com/templates/com/screen_slideshow.jinja b/com/templates/com/screen_slideshow.jinja index 448f8dfc..0374257e 100644 --- a/com/templates/com/screen_slideshow.jinja +++ b/com/templates/com/screen_slideshow.jinja @@ -3,7 +3,7 @@ {% trans %}Slideshow{% endtrans %} - + diff --git a/com/views.py b/com/views.py index 1b7ab8bc..f9993b3c 100644 --- a/com/views.py +++ b/com/views.py @@ -685,8 +685,12 @@ class PosterEditBaseView(UpdateView): def get_initial(self): return { - "date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S"), - "date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S"), + "date_begin": self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S") + if self.object.date_begin + else None, + "date_end": self.object.date_end.strftime("%Y-%m-%d %H:%M:%S") + if self.object.date_end + else None, } def dispatch(self, request, *args, **kwargs): From 0d1629495bea7910f3d02b1428f0c59a1869eccc Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 28 Dec 2024 18:54:42 +0100 Subject: [PATCH 15/40] Refactor com scss and add basic unified event calendar --- com/api.py | 53 ++ .../com/components/ics-calendar-index.ts | 75 +++ com/static/com/components/ics-calendar.scss | 60 ++ com/static/com/css/news-detail.scss | 66 ++ com/static/com/css/news-list.scss | 298 +++++++++ com/static/com/css/posters.scss | 230 +++++++ com/templates/com/news_detail.jinja | 5 + com/templates/com/news_list.jinja | 25 +- com/templates/com/poster_list.jinja | 4 + com/templates/com/poster_moderate.jinja | 4 + core/management/commands/populate.py | 4 +- core/static/core/devices.scss | 5 + core/static/core/style.scss | 600 +----------------- core/templates/core/poster_list.jinja | 54 -- package-lock.json | 52 ++ package.json | 7 +- poetry.lock | 84 ++- pyproject.toml | 1 + sith/settings.py | 1 + tsconfig.json | 3 +- 20 files changed, 963 insertions(+), 668 deletions(-) create mode 100644 com/api.py create mode 100644 com/static/bundled/com/components/ics-calendar-index.ts create mode 100644 com/static/com/components/ics-calendar.scss create mode 100644 com/static/com/css/news-detail.scss create mode 100644 com/static/com/css/news-list.scss create mode 100644 com/static/com/css/posters.scss create mode 100644 core/static/core/devices.scss delete mode 100644 core/templates/core/poster_list.jinja diff --git a/com/api.py b/com/api.py new file mode 100644 index 00000000..64f375fd --- /dev/null +++ b/com/api.py @@ -0,0 +1,53 @@ +from datetime import timedelta + +import urllib3 +from django.core.cache import cache +from django.http import HttpResponse +from django.utils import timezone +from ics import Calendar, Event +from ninja_extra import ControllerBase, api_controller, route + +from com.models import NewsDate + + +@api_controller("/calendar") +class CalendarController(ControllerBase): + @route.get("/external.ics") + def calendar_external(self): + CACHE_KEY = "external_calendar" + if cached := cache.get(CACHE_KEY): + return HttpResponse( + cached, + content_type="text/calendar", + status=200, + ) + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status == 200: + cache.set(CACHE_KEY, calendar.data, 3600) # Cache for one hour + return HttpResponse( + calendar.data, + content_type="text/calendar", + status=calendar.status, + ) + + @route.get("/internal.ics") + def calendar_internal(self): + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + start_date__lte=timezone.now() + timedelta(days=30), + ).prefetch_related("news"): + event = Event( + name=news_date.news.title, + begin=news_date.start_date, + end=news_date.end_date, + ) + calendar.events.add(event) + + return HttpResponse( + calendar.serialize().encode("utf-8"), + content_type="text/calendar", + ) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts new file mode 100644 index 00000000..f88b9b0f --- /dev/null +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -0,0 +1,75 @@ +import { makeUrl } from "#core:utils/api"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; +import { Calendar } from "@fullcalendar/core"; +import enLocale from "@fullcalendar/core/locales/en-gb"; +import frLocale from "@fullcalendar/core/locales/fr"; +import dayGridPlugin from "@fullcalendar/daygrid"; +import iCalendarPlugin from "@fullcalendar/icalendar"; +import listPlugin from "@fullcalendar/list"; +import { calendarCalendarExternal, calendarCalendarInternal } from "#openapi"; + +@registerComponent("ics-calendar") +export class IcsCalendar extends inheritHtmlElement("div") { + static observedAttributes = ["locale"]; + private calendar: Calendar; + private locale = "en"; + + attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) { + if (name !== "locale") { + return; + } + + this.locale = newValue; + } + + isMobile() { + return window.innerWidth < 765; + } + + currentView() { + // Get view type based on viewport + return this.isMobile() ? "listMonth" : "dayGridMonth"; + } + + currentToolbar() { + if (this.isMobile()) { + return { + left: "prev,next", + center: "title", + right: "", + }; + } + return { + left: "prev,next today", + center: "title", + right: "dayGridMonth,dayGridWeek,dayGridDay", + }; + } + + async connectedCallback() { + super.connectedCallback(); + this.calendar = new Calendar(this.node, { + plugins: [dayGridPlugin, iCalendarPlugin, listPlugin], + locales: [frLocale, enLocale], + height: "auto", + locale: this.locale, + initialView: this.currentView(), + headerToolbar: this.currentToolbar(), + eventSources: [ + { + url: await makeUrl(calendarCalendarInternal), + format: "ics", + }, + { + url: await makeUrl(calendarCalendarExternal), + format: "ics", + }, + ], + windowResize: () => { + this.calendar.changeView(this.currentView()); + this.calendar.setOption("headerToolbar", this.currentToolbar()); + }, + }); + this.calendar.render(); + } +} diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss new file mode 100644 index 00000000..bb858dd5 --- /dev/null +++ b/com/static/com/components/ics-calendar.scss @@ -0,0 +1,60 @@ +@import "core/static/core/colors"; + + +:root { + --fc-button-border-color: #fff; + --fc-button-hover-border-color: #fff; + --fc-button-active-border-color: #fff; + --fc-button-text-color: #fff; + --fc-button-bg-color: #1a78b3; + --fc-button-active-bg-color: #15608F; + --fc-button-hover-bg-color: #15608F; + --fc-today-bg-color: rgba(26, 120, 179, 0.1); + --fc-border-color: #DDDDDD; + --sc-main-background-color: #f9fafb; + --sc-main-padding: 5px; + --sc-main-border: 0px solid #DDDDDD; + --sc-main-border-radius: 0px; + --sc-body-font-family: Roboto; + --sc-title-font-family: Roboto; + --sc-body-font-size: 16px; + --sc-title-font-size: 28px; + --sc-body-font-weight: 400; + --sc-title-font-weight: 500; + --sc-title-font-color: #111111; + --sc-base-body-font-color: #222222; + --sc-title-font-style: normal; + --sc-body-font-style: normal; + --sc-event-dot-color: #1a78b3; + --sc-button-border: 1px solid #ffffff; + --sc-button-border-radius: 4px; + --sc-button-icons-size: 22px; + --sc-grid-event-white-space: nowrap; + --sc-block-event-background-color-hovered: rgb(245, 245, 245); + --sc-block-event-border: 1px solid rgba(255, 255, 255, 0); + --sc-block-event-border-radius: 2.5px; + --sc-dot-event-background-color: rgba(255, 255, 255, 0); + --sc-dot-event-background-color-hovered: rgb(245, 245, 245); + --sc-dot-event-text-color: #222222; + --sc-dot-event-border: 1px solid rgba(255, 255, 255, 0); + --sc-dot-event-border-radius: 2.5px; + --sc-grid-day-header-background-color: rgba(255, 255, 255, 0); + --sc-list-day-header-background-color: rgba(208, 208, 208, 0.3); + --sc-inner-calendar-background-color: rgba(255, 255, 255, 0); + --sc-past-day-background-color: rgba(255, 255, 255, 0); + --sc-future-day-background-color: rgba(255, 255, 255, 0); + --sc-disabled-day-background-color: rgba(208, 208, 208, 0.3); + --sc-event-overlay-background-color: #FFFFFF; + --sc-event-overlay-padding: 20px; + --sc-event-overlay-border: 1px solid #EEEEEE; + --sc-event-overlay-border-radius: 4px; + --sc-event-overlay-primary-icon-color: #1a78b3; + --sc-event-overlay-secondary-icon-color: #000000; + --sc-event-overlay-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); + --sc-event-overlay-max-width: 600px; +} + +ics-calendar { + border: none; + box-shadow: none; +} \ No newline at end of file diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss new file mode 100644 index 00000000..0a07e62d --- /dev/null +++ b/com/static/com/css/news-detail.scss @@ -0,0 +1,66 @@ +@import "core/static/core/colors"; + +#news_details { + display: inline-block; + margin-top: 20px; + padding: 0.4em; + width: 80%; + background: $white-color; + + h4 { + margin-top: 1em; + text-transform: uppercase; + } + + .club_logo { + display: inline-block; + text-align: center; + width: 19%; + float: left; + min-width: 15em; + margin: 0; + + img { + max-height: 15em; + max-width: 12em; + display: block; + margin: 0 auto; + margin-bottom: 10px; + } + } + + .share_button { + border: none; + color: white; + padding: 0.5em 1em; + text-align: center; + text-decoration: none; + font-size: 1.2em; + border-radius: 2px; + float: right; + display: block; + margin-left: 0.3em; + + &:hover { + color: lightgrey; + } + } + + .facebook { + background: $faceblue; + } + + .twitter { + background: $twitblue; + } + + .news_meta { + margin-top: 10em; + font-size: small; + } +} + +.helptext { + margin-top: 10px; + display: block; +} \ No newline at end of file diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss new file mode 100644 index 00000000..a33e6315 --- /dev/null +++ b/com/static/com/css/news-list.scss @@ -0,0 +1,298 @@ +@import "core/static/core/colors"; +@import "core/static/core/devices"; + +#news { + display: flex; + + @media (max-width: 800px) { + flex-direction: column; + } + + .news_column { + display: inline-block; + margin: 0; + vertical-align: top; + } + + #news_admin { + margin-bottom: 1em; + } + + #right_column { + flex: 20%; + float: right; + margin: 0.2em; + } + + #left_column { + flex: 79%; + margin: 0.2em; + + h3 { + background: $second-color; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.4em; + margin: 0 0 0.5em 0; + text-transform: uppercase; + font-size: 1.1em; + + &:not(:first-of-type) { + margin: 2em 0 1em 0; + } + } + } + + @media screen and (max-width: $small-devices) { + + #left_column, + #right_column { + flex: 100%; + } + } + + /* AGENDA/BIRTHDAYS */ + #agenda, + #birthdays { + display: block; + width: 100%; + background: white; + font-size: 70%; + margin-bottom: 1em; + + #agenda_title, + #birthdays_title { + margin: 0; + border-radius: 5px 5px 0 0; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.5em; + font-weight: bold; + font-size: 150%; + text-align: center; + text-transform: uppercase; + background: $second-color; + } + + #agenda_content { + overflow: auto; + box-shadow: $shadow-color 1px 1px 1px; + height: 20em; + } + + #agenda_content, + #birthdays_content { + .agenda_item { + padding: 0.5em; + margin-bottom: 0.5em; + + &:nth-of-type(even) { + background: $secondary-neutral-light-color; + } + + .agenda_time { + font-size: 90%; + color: grey; + } + + .agenda_item_content { + p { + margin-top: 0.2em; + } + } + } + + ul.birthdays_year { + margin: 0; + list-style-type: none; + font-weight: bold; + + >li { + padding: 0.5em; + + &:nth-child(even) { + background: $secondary-neutral-light-color; + } + } + + ul { + margin: 0; + margin-left: 1em; + list-style-type: square; + list-style-position: inside; + font-weight: normal; + } + } + } + } + + /* END AGENDA/BIRTHDAYS */ + + /* EVENTS TODAY AND NEXT FEW DAYS */ + .news_events_group { + box-shadow: $shadow-color 1px 1px 1px; + margin-left: 1em; + margin-bottom: 0.5em; + + .news_events_group_date { + display: table-cell; + padding: 0.6em; + vertical-align: middle; + background: $primary-neutral-dark-color; + color: $white-color; + text-transform: uppercase; + text-align: center; + font-weight: bold; + font-family: monospace; + font-size: 1.4em; + border-radius: 7px 0 0 7px; + + div { + margin: 0 auto; + + .day { + font-size: 1.5em; + } + } + } + + .news_events_group_items { + display: table-cell; + width: 100%; + + .news_event:nth-of-type(odd) { + background: white; + } + + .news_event:nth-of-type(even) { + background: $primary-neutral-light-color; + } + + .news_event { + display: block; + padding: 0.4em; + + &:not(:last-child) { + border-bottom: 1px solid grey; + } + + div { + margin: 0.2em; + } + + h4 { + margin-top: 1em; + text-transform: uppercase; + } + + .club_logo { + float: left; + min-width: 7em; + max-width: 9em; + margin: 0; + margin-right: 1em; + margin-top: 0.8em; + + img { + max-height: 6em; + max-width: 8em; + display: block; + margin: 0 auto; + } + } + + .news_date { + font-size: 100%; + } + + .news_content { + clear: left; + + .button_bar { + text-align: right; + + .fb { + color: $faceblue; + } + + .twitter { + color: $twitblue; + } + } + } + } + } + } + + /* END EVENTS TODAY AND NEXT FEW DAYS */ + + /* COMING SOON */ + .news_coming_soon { + display: list-item; + list-style-type: square; + list-style-position: inside; + margin-left: 1em; + padding-left: 0; + + a { + font-weight: bold; + text-transform: uppercase; + } + + .news_date { + font-size: 0.9em; + } + } + + /* END COMING SOON */ + + /* NOTICES */ + .news_notice { + margin: 0 0 1em 1em; + padding: 0.4em; + padding-left: 1em; + background: $secondary-neutral-light-color; + box-shadow: $shadow-color 0 0 2px; + border-radius: 18px 5px 18px 5px; + + h4 { + margin: 0; + } + + .news_content { + margin-left: 1em; + } + } + + /* END NOTICES */ + + /* CALLS */ + .news_call { + margin: 0 0 1em 1em; + padding: 0.4em; + padding-left: 1em; + background: $secondary-neutral-light-color; + border: 1px solid grey; + box-shadow: $shadow-color 1px 1px 1px; + + h4 { + margin: 0; + } + + .news_date { + font-size: 0.9em; + } + + .news_content { + margin-left: 1em; + } + } + + /* END CALLS */ + + .news_empty { + margin-left: 1em; + } + + .news_date { + color: grey; + } +} \ No newline at end of file diff --git a/com/static/com/css/posters.scss b/com/static/com/css/posters.scss new file mode 100644 index 00000000..26cf2b91 --- /dev/null +++ b/com/static/com/css/posters.scss @@ -0,0 +1,230 @@ +#poster_list, +#screen_list, +#poster_edit, +#screen_edit { + position: relative; + + #title { + position: relative; + padding: 10px; + margin: 10px; + border-bottom: 2px solid black; + + h3 { + display: flex; + justify-content: center; + align-items: center; + } + + #links { + position: absolute; + display: flex; + bottom: 5px; + + &.left { + left: 0; + } + + &.right { + right: 0; + } + + .link { + padding: 5px; + padding-left: 20px; + padding-right: 20px; + margin-left: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &.delete { + background-color: hsl(0, 100%, 40%); + } + } + } + } + + #posters, + #screens { + position: relative; + display: flex; + flex-wrap: wrap; + + #no-posters, + #no-screens { + display: flex; + justify-content: center; + align-items: center; + } + + .poster, + .screen { + min-width: 10%; + max-width: 20%; + display: flex; + flex-direction: column; + margin: 10px; + border: 2px solid darkgrey; + border-radius: 4px; + padding: 10px; + background-color: lightgrey; + + * { + display: flex; + justify-content: center; + align-items: center; + } + + .name { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + } + + .image { + flex-grow: 1; + position: relative; + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + img { + max-height: 20vw; + max-width: 100%; + } + + &:hover { + &::before { + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + top: 0; + left: 0; + z-index: 10; + content: "Click to expand"; + color: white; + background-color: rgba(black, 0.5); + } + } + } + + .dates { + padding-bottom: 5px; + margin-bottom: 5px; + border-bottom: 1px solid whitesmoke; + + * { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-left: 5px; + margin-right: 5px; + } + + .begin, + .end { + width: 48%; + } + + .begin { + border-right: 1px solid whitesmoke; + padding-right: 2%; + } + } + + .edit, + .moderate, + .slideshow { + padding: 5px; + border-radius: 20px; + background-color: hsl(40, 100%, 50%); + color: black; + + &:hover { + color: black; + background-color: hsl(40, 58%, 50%); + } + + &:nth-child(2n) { + margin-top: 5px; + margin-bottom: 5px; + } + } + + .tooltip { + visibility: hidden; + width: 120px; + background-color: hsl(210, 20%, 98%); + color: hsl(0, 0%, 0%); + text-align: center; + padding: 5px 0; + border-radius: 6px; + position: absolute; + z-index: 10; + + ul { + margin-left: 0; + display: inline-block; + + li { + display: list-item; + list-style-type: none; + } + } + } + + &.not_moderated { + border: 1px solid red; + } + + &:hover .tooltip { + visibility: visible; + } + } + } + + #view { + position: fixed; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + z-index: 10; + visibility: hidden; + background-color: rgba(10, 10, 10, 0.9); + overflow: hidden; + + &.active { + visibility: visible; + } + + #placeholder { + width: 80vw; + height: 80vh; + display: flex; + justify-content: center; + align-items: center; + top: 0; + left: 0; + + img { + max-width: 100%; + max-height: 100%; + } + } + } +} \ No newline at end of file diff --git a/com/templates/com/news_detail.jinja b/com/templates/com/news_detail.jinja index cbfa596c..238515ed 100644 --- a/com/templates/com/news_detail.jinja +++ b/com/templates/com/news_detail.jinja @@ -11,6 +11,11 @@ {{ gen_news_metatags(news) }} {% endblock %} + +{% block additional_css %} + +{% endblock %} + {% block content %}

{% trans %}Back to news{% endtrans %}

diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index ac9a7892..3f2444f1 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -5,6 +5,15 @@ {% trans %}News{% endtrans %} {% endblock %} +{% block additional_css %} + + +{% endblock %} + +{% block additional_js %} + +{% endblock %} + {% block content %} {% if user.is_com_admin %}
@@ -98,16 +107,6 @@ type="EVENT").order_by('dates__start_date') %} {% endfor %} {% endif %} -

{% trans %}All coming events{% endtrans %}

- -
-
{% trans %}Agenda{% endtrans %}
@@ -154,8 +153,14 @@ type="EVENT").order_by('dates__start_date') %} {%- endif -%}
+ +

{% trans %}All coming events{% endtrans %}

+ + + + {% endblock %} diff --git a/com/templates/com/poster_list.jinja b/com/templates/com/poster_list.jinja index 8c4f5cd1..c9af62c0 100644 --- a/com/templates/com/poster_list.jinja +++ b/com/templates/com/poster_list.jinja @@ -10,6 +10,10 @@ {% trans %}Poster{% endtrans %} {% endblock %} +{% block additional_css %} + +{% endblock %} + {% block content %}
diff --git a/com/templates/com/poster_moderate.jinja b/com/templates/com/poster_moderate.jinja index 36e3dae7..6370becf 100644 --- a/com/templates/com/poster_moderate.jinja +++ b/com/templates/com/poster_moderate.jinja @@ -5,6 +5,10 @@ {% endblock %} +{% block additional_css %} + +{% endblock %} + {% block content %}
diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 9cf9c59b..222cc509 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -738,7 +738,7 @@ Welcome to the wiki page! NewsDate( news=n, start_date=friday + timedelta(hours=24 * 7 + 1), - end_date=self.now + timedelta(hours=24 * 7 + 9), + end_date=friday + timedelta(hours=24 * 7 + 9), ) ) # Weekly @@ -765,7 +765,7 @@ Welcome to the wiki page! ) NewsDate.objects.bulk_create(news_dates) - # Create som data for pedagogy + # Create some data for pedagogy UV( code="PA00", diff --git a/core/static/core/devices.scss b/core/static/core/devices.scss new file mode 100644 index 00000000..25839f24 --- /dev/null +++ b/core/static/core/devices.scss @@ -0,0 +1,5 @@ +/*--------------------------MEDIA QUERY HELPERS------------------------*/ + +$small-devices: 576px; +$medium-devices: 768px; +$large-devices: 992px; \ No newline at end of file diff --git a/core/static/core/style.scss b/core/static/core/style.scss index a9205e23..2f3af9f7 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -1,10 +1,6 @@ @import "colors"; @import "forms"; - -/*--------------------------MEDIA QUERY HELPERS------------------------*/ -$small-devices: 576px; -$medium-devices: 768px; -$large-devices: 992px; +@import "devices"; /*--------------------------------GENERAL------------------------------*/ @@ -453,302 +449,6 @@ body { } } - /*---------------------------------NEWS--------------------------------*/ - #news { - display: flex; - - @media (max-width: 800px) { - flex-direction: column; - } - - .news_column { - display: inline-block; - margin: 0; - vertical-align: top; - } - - #news_admin { - margin-bottom: 1em; - } - - #right_column { - flex: 20%; - float: right; - margin: 0.2em; - } - - #left_column { - flex: 79%; - margin: 0.2em; - - h3 { - background: $second-color; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.4em; - margin: 0 0 0.5em 0; - text-transform: uppercase; - font-size: 1.1em; - - &:not(:first-of-type) { - margin: 2em 0 1em 0; - } - } - } - - @media screen and (max-width: $small-devices) { - - #left_column, - #right_column { - flex: 100%; - } - } - - /* AGENDA/BIRTHDAYS */ - #agenda, - #birthdays { - display: block; - width: 100%; - background: white; - font-size: 70%; - margin-bottom: 1em; - - #agenda_title, - #birthdays_title { - margin: 0; - border-radius: 5px 5px 0 0; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.5em; - font-weight: bold; - font-size: 150%; - text-align: center; - text-transform: uppercase; - background: $second-color; - } - - #agenda_content { - overflow: auto; - box-shadow: $shadow-color 1px 1px 1px; - height: 20em; - } - - #agenda_content, - #birthdays_content { - .agenda_item { - padding: 0.5em; - margin-bottom: 0.5em; - - &:nth-of-type(even) { - background: $secondary-neutral-light-color; - } - - .agenda_time { - font-size: 90%; - color: grey; - } - - .agenda_item_content { - p { - margin-top: 0.2em; - } - } - } - - ul.birthdays_year { - margin: 0; - list-style-type: none; - font-weight: bold; - - >li { - padding: 0.5em; - - &:nth-child(even) { - background: $secondary-neutral-light-color; - } - } - - ul { - margin: 0; - margin-left: 1em; - list-style-type: square; - list-style-position: inside; - font-weight: normal; - } - } - } - } - - /* END AGENDA/BIRTHDAYS */ - - /* EVENTS TODAY AND NEXT FEW DAYS */ - .news_events_group { - box-shadow: $shadow-color 1px 1px 1px; - margin-left: 1em; - margin-bottom: 0.5em; - - .news_events_group_date { - display: table-cell; - padding: 0.6em; - vertical-align: middle; - background: $primary-neutral-dark-color; - color: $white-color; - text-transform: uppercase; - text-align: center; - font-weight: bold; - font-family: monospace; - font-size: 1.4em; - border-radius: 7px 0 0 7px; - - div { - margin: 0 auto; - - .day { - font-size: 1.5em; - } - } - } - - .news_events_group_items { - display: table-cell; - width: 100%; - - .news_event:nth-of-type(odd) { - background: white; - } - - .news_event:nth-of-type(even) { - background: $primary-neutral-light-color; - } - - .news_event { - display: block; - padding: 0.4em; - - &:not(:last-child) { - border-bottom: 1px solid grey; - } - - div { - margin: 0.2em; - } - - h4 { - margin-top: 1em; - text-transform: uppercase; - } - - .club_logo { - float: left; - min-width: 7em; - max-width: 9em; - margin: 0; - margin-right: 1em; - margin-top: 0.8em; - - img { - max-height: 6em; - max-width: 8em; - display: block; - margin: 0 auto; - } - } - - .news_date { - font-size: 100%; - } - - .news_content { - clear: left; - - .button_bar { - text-align: right; - - .fb { - color: $faceblue; - } - - .twitter { - color: $twitblue; - } - } - } - } - } - } - - /* END EVENTS TODAY AND NEXT FEW DAYS */ - - /* COMING SOON */ - .news_coming_soon { - display: list-item; - list-style-type: square; - list-style-position: inside; - margin-left: 1em; - padding-left: 0; - - a { - font-weight: bold; - text-transform: uppercase; - } - - .news_date { - font-size: 0.9em; - } - } - - /* END COMING SOON */ - - /* NOTICES */ - .news_notice { - margin: 0 0 1em 1em; - padding: 0.4em; - padding-left: 1em; - background: $secondary-neutral-light-color; - box-shadow: $shadow-color 0 0 2px; - border-radius: 18px 5px 18px 5px; - - h4 { - margin: 0; - } - - .news_content { - margin-left: 1em; - } - } - - /* END NOTICES */ - - /* CALLS */ - .news_call { - margin: 0 0 1em 1em; - padding: 0.4em; - padding-left: 1em; - background: $secondary-neutral-light-color; - border: 1px solid grey; - box-shadow: $shadow-color 1px 1px 1px; - - h4 { - margin: 0; - } - - .news_date { - font-size: 0.9em; - } - - .news_content { - margin-left: 1em; - } - } - - /* END CALLS */ - - .news_empty { - margin-left: 1em; - } - - .news_date { - color: grey; - } - } } @media screen and (max-width: $small-devices) { @@ -757,304 +457,6 @@ body { } } -#news_details { - display: inline-block; - margin-top: 20px; - padding: 0.4em; - width: 80%; - background: $white-color; - - h4 { - margin-top: 1em; - text-transform: uppercase; - } - - .club_logo { - display: inline-block; - text-align: center; - width: 19%; - float: left; - min-width: 15em; - margin: 0; - - img { - max-height: 15em; - max-width: 12em; - display: block; - margin: 0 auto; - margin-bottom: 10px; - } - } - - .share_button { - border: none; - color: white; - padding: 0.5em 1em; - text-align: center; - text-decoration: none; - font-size: 1.2em; - border-radius: 2px; - float: right; - display: block; - margin-left: 0.3em; - - &:hover { - color: lightgrey; - } - } - - .facebook { - background: $faceblue; - } - - .twitter { - background: $twitblue; - } - - .news_meta { - margin-top: 10em; - font-size: small; - } -} - -.helptext { - margin-top: 10px; - display: block; -} - -/*---------------------------POSTERS----------------------------*/ - -#poster_list, -#screen_list, -#poster_edit, -#screen_edit { - position: relative; - - #title { - position: relative; - padding: 10px; - margin: 10px; - border-bottom: 2px solid black; - - h3 { - display: flex; - justify-content: center; - align-items: center; - } - - #links { - position: absolute; - display: flex; - bottom: 5px; - - &.left { - left: 0; - } - - &.right { - right: 0; - } - - .link { - padding: 5px; - padding-left: 20px; - padding-right: 20px; - margin-left: 5px; - border-radius: 20px; - background-color: hsl(40, 100%, 50%); - color: black; - - &:hover { - color: black; - background-color: hsl(40, 58%, 50%); - } - - &.delete { - background-color: hsl(0, 100%, 40%); - } - } - } - } - - #posters, - #screens { - position: relative; - display: flex; - flex-wrap: wrap; - - #no-posters, - #no-screens { - display: flex; - justify-content: center; - align-items: center; - } - - .poster, - .screen { - min-width: 10%; - max-width: 20%; - display: flex; - flex-direction: column; - margin: 10px; - border: 2px solid darkgrey; - border-radius: 4px; - padding: 10px; - background-color: lightgrey; - - * { - display: flex; - justify-content: center; - align-items: center; - } - - .name { - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - } - - .image { - flex-grow: 1; - position: relative; - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - - img { - max-height: 20vw; - max-width: 100%; - } - - &:hover { - &::before { - position: absolute; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - top: 0; - left: 0; - z-index: 10; - content: "Click to expand"; - color: white; - background-color: rgba(black, 0.5); - } - } - } - - .dates { - padding-bottom: 5px; - margin-bottom: 5px; - border-bottom: 1px solid whitesmoke; - - * { - display: flex; - justify-content: center; - align-items: center; - flex-wrap: wrap; - margin-left: 5px; - margin-right: 5px; - } - - .begin, - .end { - width: 48%; - } - - .begin { - border-right: 1px solid whitesmoke; - padding-right: 2%; - } - } - - .edit, - .moderate, - .slideshow { - padding: 5px; - border-radius: 20px; - background-color: hsl(40, 100%, 50%); - color: black; - - &:hover { - color: black; - background-color: hsl(40, 58%, 50%); - } - - &:nth-child(2n) { - margin-top: 5px; - margin-bottom: 5px; - } - } - - .tooltip { - visibility: hidden; - width: 120px; - background-color: hsl(210, 20%, 98%); - color: hsl(0, 0%, 0%); - text-align: center; - padding: 5px 0; - border-radius: 6px; - position: absolute; - z-index: 10; - - ul { - margin-left: 0; - display: inline-block; - - li { - display: list-item; - list-style-type: none; - } - } - } - - &.not_moderated { - border: 1px solid red; - } - - &:hover .tooltip { - visibility: visible; - } - } - } - - #view { - position: fixed; - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; - top: 0; - left: 0; - z-index: 10; - visibility: hidden; - background-color: rgba(10, 10, 10, 0.9); - overflow: hidden; - - &.active { - visibility: visible; - } - - #placeholder { - width: 80vw; - height: 80vh; - display: flex; - justify-content: center; - align-items: center; - top: 0; - left: 0; - - img { - max-width: 100%; - max-height: 100%; - } - } - } -} - /*---------------------------ACCOUNTING----------------------------*/ #accounting { .journal-table { diff --git a/core/templates/core/poster_list.jinja b/core/templates/core/poster_list.jinja deleted file mode 100644 index fe65658c..00000000 --- a/core/templates/core/poster_list.jinja +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "core/base.jinja" %} - -{% block script %} - {{ super() }} - -{% endblock %} - - -{% block title %} - {% trans %}Poster{% endtrans %} -{% endblock %} - -{% block content %} -
- -
-

{% trans %}Posters{% endtrans %}

- -
- -
- - {% if poster_list.count() == 0 %} -
{% trans %}No posters{% endtrans %}
- {% else %} - - {% for poster in poster_list %} -
-
{{ poster.name }}
-
-
-
{{ poster.date_begin | date("d/M/Y H:m") }}
-
{{ poster.date_end | date("d/M/Y H:m") }}
-
- {% trans %}Edit{% endtrans %} -
- {% endfor %} - - {% endif %} - -
- -
- -
-{% endblock %} - - - diff --git a/package-lock.json b/package-lock.json index 9b49ac0e..bfa05f40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,10 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@fortawesome/fontawesome-free": "^6.6.0", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/icalendar": "^6.1.15", + "@fullcalendar/list": "^6.1.15", "@hey-api/client-fetch": "^0.4.0", "@sentry/browser": "^8.34.0", "@zip.js/zip.js": "^2.7.52", @@ -2384,6 +2388,39 @@ "node": ">=6" } }, + "node_modules/@fullcalendar/core": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz", + "integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==", + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz", + "integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, + "node_modules/@fullcalendar/icalendar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/icalendar/-/icalendar-6.1.15.tgz", + "integrity": "sha512-iroDc02fjxWCEYE9Lg8x+4HCJTrt04ZgDddwm0LLaWUbtx24rEcnzJP34NUx0KOTLsBjel6U/33lXvU9qDCrhg==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15", + "ical.js": "^1.4.0" + } + }, + "node_modules/@fullcalendar/list": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.15.tgz", + "integrity": "sha512-U1bce04tYDwkFnuVImJSy2XalYIIQr6YusOWRPM/5ivHcJh67Gm8CIMSWpi3KdRSNKFkqBxLPkfZGBMaOcJYug==", + "peerDependencies": { + "@fullcalendar/core": "~6.1.15" + } + }, "node_modules/@hey-api/client-fetch": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.4.0.tgz", @@ -4162,6 +4199,12 @@ "node": ">=16.17.0" } }, + "node_modules/ical.js": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-1.5.0.tgz", + "integrity": "sha512-7ZxMkogUkkaCx810yp0ZGKvq1ZpRgJeornPttpoxe6nYZ3NLesZe1wWMXDdwTkj/b5NtXT+Y16Aakph/ao98ZQ==", + "peer": true + }, "node_modules/import-from-esm": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.4.tgz", @@ -4924,6 +4967,15 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 9721eea4..379fc782 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "#openapi": "./staticfiles/generated/openapi/index.ts", "#core:*": "./core/static/bundled/*", "#pedagogy:*": "./pedagogy/static/bundled/*", - "#counter:*": "./counter/static/bundled/*" + "#counter:*": "./counter/static/bundled/*", + "#com:*": "./com/static/bundled/*" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -36,6 +37,10 @@ "dependencies": { "@alpinejs/sort": "^3.14.7", "@fortawesome/fontawesome-free": "^6.6.0", + "@fullcalendar/core": "^6.1.15", + "@fullcalendar/daygrid": "^6.1.15", + "@fullcalendar/icalendar": "^6.1.15", + "@fullcalendar/list": "^6.1.15", "@hey-api/client-fetch": "^0.4.0", "@sentry/browser": "^8.34.0", "@zip.js/zip.js": "^2.7.52", diff --git a/poetry.lock b/poetry.lock index 311df18a..3c4c08b0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,6 +22,25 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + [[package]] name = "asgiref" version = "3.8.1" @@ -51,6 +70,25 @@ files = [ astroid = ["astroid (>=2,<4)"] test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "babel" version = "2.16.0" @@ -931,6 +969,24 @@ files = [ {file = "hiredis-3.1.0.tar.gz", hash = "sha256:51d40ac3611091020d7dea6b05ed62cb152bff595fa4f931e7b6479d777acf7c"}, ] +[[package]] +name = "ics" +version = "0.7.2" +description = "Python icalendar (rfc5545) parser" +optional = false +python-versions = "*" +files = [ + {file = "ics-0.7.2-py2.py3-none-any.whl", hash = "sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103"}, + {file = "ics-0.7.2.tar.gz", hash = "sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05"}, +] + +[package.dependencies] +arrow = ">=0.11" +attrs = ">=19.1.0" +python-dateutil = "*" +six = ">1.5" +tatsu = ">4.2" + [[package]] name = "identify" version = "2.6.3" @@ -2524,6 +2580,21 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "tatsu" +version = "5.12.2" +description = "TatSu takes a grammar in a variation of EBNF as input, and outputs a memoizing PEG/Packrat parser in Python." +optional = false +python-versions = ">=3.11" +files = [ + {file = "TatSu-5.12.2-py3-none-any.whl", hash = "sha256:9c313186ae5262662cb3fbec52c9a12db1ef752e615f46cac3eb568cb91eacf9"}, + {file = "tatsu-5.12.2.tar.gz", hash = "sha256:5894dc7ddba9a1886a95ff2f06cef1be2b3d3a37c776eba8177ef4dcd80ccb03"}, +] + +[package.extras] +colorization = ["colorama"] +parproc = ["rich"] + [[package]] name = "tomli" version = "2.2.1" @@ -2580,6 +2651,17 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -2724,4 +2806,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5836c1a8ad42645d7d045194c8c371754b19957ebdcd2aaa902a2fb3dc97cc53" +content-hash = "f0acbbe66fd99ac04891bcc8a5f28167a927e0b1f3677ebd8ab302a0e2fb9be2" diff --git a/pyproject.toml b/pyproject.toml index be892cdf..f3427faf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ Sphinx = "^5" # Needed for building xapian tomli = "^2.2.1" django-honeypot = "^1.2.1" pydantic-extra-types = "^2.10.1" +ics = "^0.7.2" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development diff --git a/sith/settings.py b/sith/settings.py index 5fdc3786..a88734d3 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -163,6 +163,7 @@ TEMPLATES = [ "ProductType": "counter.models.ProductType", "timezone": "django.utils.timezone", "get_sith": "com.views.sith", + "get_language": "django.utils.translation.get_language", }, "bytecode_cache": { "name": "default", diff --git a/tsconfig.json b/tsconfig.json index 7b3be5fc..aaee9330 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "#openapi": ["./staticfiles/generated/openapi/index.ts"], "#core:*": ["./core/static/bundled/*"], "#pedagogy:*": ["./pedagogy/static/bundled/*"], - "#counter:*": ["./counter/static/bundled/*"] + "#counter:*": ["./counter/static/bundled/*"], + "#com:*": ["./com/static/bundled/*"] } } } From 6d7467e746989c0a9be62a55f93e6d09b38ada89 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 30 Dec 2024 14:41:41 +0100 Subject: [PATCH 16/40] Make new calendar look like the iframe one --- com/static/com/components/ics-calendar.scss | 39 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index bb858dd5..25a66c34 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -44,12 +44,12 @@ --sc-past-day-background-color: rgba(255, 255, 255, 0); --sc-future-day-background-color: rgba(255, 255, 255, 0); --sc-disabled-day-background-color: rgba(208, 208, 208, 0.3); - --sc-event-overlay-background-color: #FFFFFF; + --sc-event-overlay-background-color: white; --sc-event-overlay-padding: 20px; --sc-event-overlay-border: 1px solid #EEEEEE; --sc-event-overlay-border-radius: 4px; --sc-event-overlay-primary-icon-color: #1a78b3; - --sc-event-overlay-secondary-icon-color: #000000; + --sc-event-overlay-secondary-icon-color: black; --sc-event-overlay-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); --sc-event-overlay-max-width: 600px; } @@ -57,4 +57,39 @@ ics-calendar { border: none; box-shadow: none; + + a.fc-col-header-cell-cushion, + a.fc-col-header-cell-cushion:hover { + color: black; + } + + a.fc-daygrid-day-number, + a.fc-daygrid-day-number:hover { + color: rgb(34, 34, 34); + } + + td { + overflow: visible; // Show events on multiple days + } + + //Reset from style.scss + table { + box-shadow: none; + border-radius: 0px; + -moz-border-radius: 0px; + margin: 0px; + } + + // Reset from style.scss + thead { + background-color: white; + color: black; + } + + // Reset from style.scss + tbody>tr { + &:nth-child(even):not(.highlight) { + background: white; + } + } } \ No newline at end of file From 48f6d134bf1ef975a00318e9be28433311ecea69 Mon Sep 17 00:00:00 2001 From: Sli Date: Mon, 30 Dec 2024 14:54:36 +0100 Subject: [PATCH 17/40] Fix news page layout --- com/templates/com/news_list.jinja | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 3f2444f1..385cbe32 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -92,20 +92,12 @@
{% endif %} -{% set coming_soon = object_list.filter(dates__start_date__gte=timezone.now()+timedelta(days=5), -type="EVENT").order_by('dates__start_date') %} -{% if coming_soon %} -

{% trans %}Coming soon... don't miss!{% endtrans %}

- {% for news in coming_soon %} -
- {{ news.title }} - {{ news.dates.first().start_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().start_date|localtime|time(DATETIME_FORMAT) }} - - {{ news.dates.first().end_date|localtime|date(DATETIME_FORMAT) }} - {{ news.dates.first().end_date|localtime|time(DATETIME_FORMAT) }} -
- {% endfor %} -{% endif %} + +

{% trans %}All coming events{% endtrans %}

+ + + +
@@ -154,10 +146,6 @@ type="EVENT").order_by('dates__start_date') %}
-

{% trans %}All coming events{% endtrans %}

- - - From eac2709e86689641339c0f225daf9495221c9337 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 31 Dec 2024 12:15:17 +0100 Subject: [PATCH 18/40] Create basic (ugly) event detail popup --- .../com/components/ics-calendar-index.ts | 80 ++++++++++++++++++- com/static/com/components/ics-calendar.scss | 80 +++++++++---------- 2 files changed, 117 insertions(+), 43 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index f88b9b0f..130dd8ef 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -1,6 +1,6 @@ import { makeUrl } from "#core:utils/api"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; -import { Calendar } from "@fullcalendar/core"; +import { Calendar, type EventClickArg } from "@fullcalendar/core"; import enLocale from "@fullcalendar/core/locales/en-gb"; import frLocale from "@fullcalendar/core/locales/fr"; import dayGridPlugin from "@fullcalendar/daygrid"; @@ -46,6 +46,71 @@ export class IcsCalendar extends inheritHtmlElement("div") { }; } + formatDate(date: Date) { + return new Intl.DateTimeFormat(this.locale, { + dateStyle: "medium", + timeStyle: "short", + }).format(date); + } + + createEventDetailPopup(event: EventClickArg) { + // Delete previous popup + const oldPopup = document.getElementById("event-details"); + if (oldPopup !== null) { + oldPopup.remove(); + } + + // Create new popup + const popup = document.createElement("div"); + const popupContainer = document.createElement("div"); + const popupFirstRow = document.createElement("div"); + const popupSecondRow = document.createElement("div"); + const popupTitleTimeIcon = document.createElement("i"); + const popupTitleTime = document.createElement("div"); + const popupTitle = document.createElement("h4"); + const popupTime = document.createElement("span"); + + popup.setAttribute("id", "event-details"); + popupContainer.setAttribute("class", "event-details-container"); + popupFirstRow.setAttribute("class", "event-details-row"); + popupSecondRow.setAttribute("class", "event-details-row"); + + popupTitleTimeIcon.setAttribute("class", "fa-solid fa-calendar-days fa-xl"); + + popupTitle.setAttribute("class", "event-details-title"); + popupTitle.textContent = event.event.title; + + popupTime.setAttribute("class", "event-details-time"); + popupTime.textContent = `${this.formatDate(event.event.start)} - ${this.formatDate(event.event.end)}`; + + popupTitleTime.appendChild(popupTitle); + popupTitleTime.appendChild(popupTime); + + popupFirstRow.appendChild(popupTitleTimeIcon); + popupSecondRow.appendChild(popupTitleTime); + + popupContainer.appendChild(popupFirstRow); + popupContainer.appendChild(popupSecondRow); + + popup.appendChild(popupContainer); + + // We can't just add the element relative to the one we want to appear under + // Otherwise, it either gets clipped by the boundaries of the calendar or resize cells + // Here, we create a popup outside the calendar that follows the clicked element + this.node.appendChild(popup); + const follow = (node: HTMLElement) => { + const rect = node.getBoundingClientRect(); + popup.setAttribute( + "style", + `top: calc(${rect.top + window.scrollY}px + ${rect.height}px); left: ${rect.left + window.scrollX}px;`, + ); + }; + follow(event.el); + window.addEventListener("resize", () => { + follow(event.el); + }); + } + async connectedCallback() { super.connectedCallback(); this.calendar = new Calendar(this.node, { @@ -69,7 +134,20 @@ export class IcsCalendar extends inheritHtmlElement("div") { this.calendar.changeView(this.currentView()); this.calendar.setOption("headerToolbar", this.currentToolbar()); }, + eventClick: (event) => { + // Avoid our popup to be deleted because we clicked outside of it + event.jsEvent.stopPropagation(); + this.createEventDetailPopup(event); + }, }); this.calendar.render(); + + window.addEventListener("click", (event: MouseEvent) => { + // Auto close popups when clicking outside of it + const popup = document.getElementById("event-details"); + if (popup !== null && !popup.contains(event.target as Node)) { + popup.remove(); + } + }); } } diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 25a66c34..825a7ebb 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -11,53 +11,49 @@ --fc-button-hover-bg-color: #15608F; --fc-today-bg-color: rgba(26, 120, 179, 0.1); --fc-border-color: #DDDDDD; - --sc-main-background-color: #f9fafb; - --sc-main-padding: 5px; - --sc-main-border: 0px solid #DDDDDD; - --sc-main-border-radius: 0px; - --sc-body-font-family: Roboto; - --sc-title-font-family: Roboto; - --sc-body-font-size: 16px; - --sc-title-font-size: 28px; - --sc-body-font-weight: 400; - --sc-title-font-weight: 500; - --sc-title-font-color: #111111; - --sc-base-body-font-color: #222222; - --sc-title-font-style: normal; - --sc-body-font-style: normal; - --sc-event-dot-color: #1a78b3; - --sc-button-border: 1px solid #ffffff; - --sc-button-border-radius: 4px; - --sc-button-icons-size: 22px; - --sc-grid-event-white-space: nowrap; - --sc-block-event-background-color-hovered: rgb(245, 245, 245); - --sc-block-event-border: 1px solid rgba(255, 255, 255, 0); - --sc-block-event-border-radius: 2.5px; - --sc-dot-event-background-color: rgba(255, 255, 255, 0); - --sc-dot-event-background-color-hovered: rgb(245, 245, 245); - --sc-dot-event-text-color: #222222; - --sc-dot-event-border: 1px solid rgba(255, 255, 255, 0); - --sc-dot-event-border-radius: 2.5px; - --sc-grid-day-header-background-color: rgba(255, 255, 255, 0); - --sc-list-day-header-background-color: rgba(208, 208, 208, 0.3); - --sc-inner-calendar-background-color: rgba(255, 255, 255, 0); - --sc-past-day-background-color: rgba(255, 255, 255, 0); - --sc-future-day-background-color: rgba(255, 255, 255, 0); - --sc-disabled-day-background-color: rgba(208, 208, 208, 0.3); - --sc-event-overlay-background-color: white; - --sc-event-overlay-padding: 20px; - --sc-event-overlay-border: 1px solid #EEEEEE; - --sc-event-overlay-border-radius: 4px; - --sc-event-overlay-primary-icon-color: #1a78b3; - --sc-event-overlay-secondary-icon-color: black; - --sc-event-overlay-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); - --sc-event-overlay-max-width: 600px; + --event-details-background-color: white; + --event-details-padding: 20px; + --event-details-border: 1px solid #EEEEEE; + --event-details-border-radius: 4px; + --event-details-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); + --event-details-max-width: 600px; } ics-calendar { border: none; box-shadow: none; + #event-details { + z-index: 10; + max-width: 1151px; + position: absolute; + + .event-details-container { + display: flex; + color: black; + flex-direction: column; + min-width: 200px; + max-width: var(--event-details-max-width); + padding: var(--event-details-padding); + border: var(--event-details-border); + border-radius: var(--event-details-border-radius); + background-color: var(--event-details-background-color); + box-shadow: var(--event-details-box-shadow); + } + + .event-details-row { + display: flex; + flex-direction: row; + align-items: start; + } + + .event-details-title { + background-color: var(--event-details-background-color); + margin-top: 0px; + margin-bottom: 4px; + } + } + a.fc-col-header-cell-cushion, a.fc-col-header-cell-cushion:hover { color: black; @@ -69,7 +65,7 @@ ics-calendar { } td { - overflow: visible; // Show events on multiple days + overflow-x: visible; // Show events on multiple days } //Reset from style.scss From fd2295119d2e3f7fdf70272116ceed58c2c23f0b Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 31 Dec 2024 15:43:18 +0100 Subject: [PATCH 19/40] nice looking popup with well aligned icon --- .../bundled/com/components/ics-calendar-index.ts | 14 +++++++------- com/static/com/components/ics-calendar.scss | 13 +++++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 130dd8ef..6bf86111 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -64,7 +64,6 @@ export class IcsCalendar extends inheritHtmlElement("div") { const popup = document.createElement("div"); const popupContainer = document.createElement("div"); const popupFirstRow = document.createElement("div"); - const popupSecondRow = document.createElement("div"); const popupTitleTimeIcon = document.createElement("i"); const popupTitleTime = document.createElement("div"); const popupTitle = document.createElement("h4"); @@ -73,24 +72,25 @@ export class IcsCalendar extends inheritHtmlElement("div") { popup.setAttribute("id", "event-details"); popupContainer.setAttribute("class", "event-details-container"); popupFirstRow.setAttribute("class", "event-details-row"); - popupSecondRow.setAttribute("class", "event-details-row"); - popupTitleTimeIcon.setAttribute("class", "fa-solid fa-calendar-days fa-xl"); + popupTitleTimeIcon.setAttribute( + "class", + "fa-solid fa-calendar-days fa-xl event-detail-row-icon", + ); - popupTitle.setAttribute("class", "event-details-title"); + popupTitle.setAttribute("class", "event-details-row-content"); popupTitle.textContent = event.event.title; - popupTime.setAttribute("class", "event-details-time"); + popupTime.setAttribute("class", "event-details-row-content"); popupTime.textContent = `${this.formatDate(event.event.start)} - ${this.formatDate(event.event.end)}`; popupTitleTime.appendChild(popupTitle); popupTitleTime.appendChild(popupTime); popupFirstRow.appendChild(popupTitleTimeIcon); - popupSecondRow.appendChild(popupTitleTime); + popupFirstRow.appendChild(popupTitleTime); popupContainer.appendChild(popupFirstRow); - popupContainer.appendChild(popupSecondRow); popup.appendChild(popupContainer); diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 825a7ebb..1483cb2b 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -41,13 +41,22 @@ ics-calendar { box-shadow: var(--event-details-box-shadow); } + .event-detail-row-icon { + margin-left: 10px; + margin-right: 20px; + align-content: center; + align-self: center; + } + .event-details-row { display: flex; - flex-direction: row; align-items: start; } - .event-details-title { + .event-details-row-content { + display: flex; + align-items: start; + flex-direction: row; background-color: var(--event-details-background-color); margin-top: 0px; margin-bottom: 4px; From 9bd14f1b4edd5e35ca626a702496ad9197e304a6 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 31 Dec 2024 15:54:50 +0100 Subject: [PATCH 20/40] Refactor popup creation --- .../com/components/ics-calendar-index.ts | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 6bf86111..227694c0 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -1,6 +1,7 @@ import { makeUrl } from "#core:utils/api"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; import { Calendar, type EventClickArg } from "@fullcalendar/core"; +import type { EventImpl } from "@fullcalendar/core/internal"; import enLocale from "@fullcalendar/core/locales/en-gb"; import frLocale from "@fullcalendar/core/locales/fr"; import dayGridPlugin from "@fullcalendar/daygrid"; @@ -60,37 +61,42 @@ export class IcsCalendar extends inheritHtmlElement("div") { oldPopup.remove(); } + const makePopupTitle = (event: EventImpl) => { + const row = document.createElement("div"); + const icon = document.createElement("i"); + const infoRow = document.createElement("div"); + const title = document.createElement("h4"); + const time = document.createElement("span"); + row.setAttribute("class", "event-details-row"); + + icon.setAttribute( + "class", + "fa-solid fa-calendar-days fa-xl event-detail-row-icon", + ); + + title.setAttribute("class", "event-details-row-content"); + title.textContent = event.title; + + time.setAttribute("class", "event-details-row-content"); + time.textContent = `${this.formatDate(event.start)} - ${this.formatDate(event.end)}`; + + infoRow.appendChild(title); + infoRow.appendChild(time); + + row.appendChild(icon); + row.appendChild(infoRow); + + return row; + }; + // Create new popup const popup = document.createElement("div"); const popupContainer = document.createElement("div"); - const popupFirstRow = document.createElement("div"); - const popupTitleTimeIcon = document.createElement("i"); - const popupTitleTime = document.createElement("div"); - const popupTitle = document.createElement("h4"); - const popupTime = document.createElement("span"); popup.setAttribute("id", "event-details"); popupContainer.setAttribute("class", "event-details-container"); - popupFirstRow.setAttribute("class", "event-details-row"); - popupTitleTimeIcon.setAttribute( - "class", - "fa-solid fa-calendar-days fa-xl event-detail-row-icon", - ); - - popupTitle.setAttribute("class", "event-details-row-content"); - popupTitle.textContent = event.event.title; - - popupTime.setAttribute("class", "event-details-row-content"); - popupTime.textContent = `${this.formatDate(event.event.start)} - ${this.formatDate(event.event.end)}`; - - popupTitleTime.appendChild(popupTitle); - popupTitleTime.appendChild(popupTime); - - popupFirstRow.appendChild(popupTitleTimeIcon); - popupFirstRow.appendChild(popupTitleTime); - - popupContainer.appendChild(popupFirstRow); + popupContainer.appendChild(makePopupTitle(event.event)); popup.appendChild(popupContainer); From e5fb87596831f42fcc8845a19b7e2fc8269b511a Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 31 Dec 2024 16:36:48 +0100 Subject: [PATCH 21/40] Add support for event location and more detail link --- com/api.py | 5 +- .../com/components/ics-calendar-index.ts | 64 +++++++++++++++---- com/static/com/components/ics-calendar.scss | 1 + 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/com/api.py b/com/api.py index 64f375fd..95e7fe92 100644 --- a/com/api.py +++ b/com/api.py @@ -3,6 +3,7 @@ from datetime import timedelta import urllib3 from django.core.cache import cache from django.http import HttpResponse +from django.urls import reverse from django.utils import timezone from ics import Calendar, Event from ninja_extra import ControllerBase, api_controller, route @@ -38,12 +39,14 @@ class CalendarController(ControllerBase): calendar = Calendar() for news_date in NewsDate.objects.filter( news__is_moderated=True, - start_date__lte=timezone.now() + timedelta(days=30), + end_date__gte=timezone.now() + - (timedelta(days=30) * 60), # Roughly get the last 6 months ).prefetch_related("news"): event = Event( name=news_date.news.title, begin=news_date.start_date, end=news_date.end_date, + url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), ) calendar.events.add(event) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index 227694c0..e3baddc6 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -61,18 +61,24 @@ export class IcsCalendar extends inheritHtmlElement("div") { oldPopup.remove(); } - const makePopupTitle = (event: EventImpl) => { + const makePopupInfo = (info: HTMLElement, iconClass: string) => { const row = document.createElement("div"); const icon = document.createElement("i"); - const infoRow = document.createElement("div"); - const title = document.createElement("h4"); - const time = document.createElement("span"); + row.setAttribute("class", "event-details-row"); - icon.setAttribute( - "class", - "fa-solid fa-calendar-days fa-xl event-detail-row-icon", - ); + icon.setAttribute("class", `event-detail-row-icon fa-xl ${iconClass}`); + + row.appendChild(icon); + row.appendChild(info); + + return row; + }; + + const makePopupTitle = (event: EventImpl) => { + const row = document.createElement("div"); + const title = document.createElement("h4"); + const time = document.createElement("span"); title.setAttribute("class", "event-details-row-content"); title.textContent = event.title; @@ -80,13 +86,33 @@ export class IcsCalendar extends inheritHtmlElement("div") { time.setAttribute("class", "event-details-row-content"); time.textContent = `${this.formatDate(event.start)} - ${this.formatDate(event.end)}`; - infoRow.appendChild(title); - infoRow.appendChild(time); + row.appendChild(title); + row.appendChild(time); + return makePopupInfo( + row, + "fa-solid fa-calendar-days fa-xl event-detail-row-icon", + ); + }; - row.appendChild(icon); - row.appendChild(infoRow); + const makePopupLocation = (event: EventImpl) => { + if (event.extendedProps.location === null) { + return null; + } + const info = document.createElement("div"); + info.innerText = event.extendedProps.location; - return row; + return makePopupInfo(info, "fa-solid fa-location-dot"); + }; + + const makePopupUrl = (event: EventImpl) => { + if (event.url === "") { + return null; + } + const url = document.createElement("a"); + url.href = event.url; + url.textContent = gettext("More info"); + + return makePopupInfo(url, "fa-solid fa-link"); }; // Create new popup @@ -98,6 +124,16 @@ export class IcsCalendar extends inheritHtmlElement("div") { popupContainer.appendChild(makePopupTitle(event.event)); + const location = makePopupLocation(event.event); + if (location !== null) { + popupContainer.appendChild(location); + } + + const url = makePopupUrl(event.event); + if (url !== null) { + popupContainer.appendChild(url); + } + popup.appendChild(popupContainer); // We can't just add the element relative to the one we want to appear under @@ -143,6 +179,8 @@ export class IcsCalendar extends inheritHtmlElement("div") { eventClick: (event) => { // Avoid our popup to be deleted because we clicked outside of it event.jsEvent.stopPropagation(); + // Don't auto-follow events URLs + event.jsEvent.preventDefault(); this.createEventDetailPopup(event); }, }); diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 1483cb2b..21aa55d7 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -39,6 +39,7 @@ ics-calendar { border-radius: var(--event-details-border-radius); background-color: var(--event-details-background-color); box-shadow: var(--event-details-box-shadow); + gap: 20px; } .event-detail-row-icon { From 169938e1da00e3d033f781b11d45c02c808b0248 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 2 Jan 2025 02:29:41 +0100 Subject: [PATCH 22/40] Replace old agenda of event with links to services and change permission to see birthdays --- com/static/com/css/news-list.scss | 95 +++++++++++++------------------ com/templates/com/news_list.jinja | 63 ++++++++++++-------- com/tests.py | 17 ++++-- locale/fr/LC_MESSAGES/django.po | 73 ++++++++++++++---------- locale/fr/LC_MESSAGES/djangojs.po | 6 +- 5 files changed, 139 insertions(+), 115 deletions(-) diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index a33e6315..4e6ca7a6 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -8,37 +8,33 @@ flex-direction: column; } - .news_column { - display: inline-block; - margin: 0; - vertical-align: top; - } - #news_admin { margin-bottom: 1em; } #right_column { flex: 20%; - float: right; - margin: 0.2em; + margin: 3.2px; + + display: inline-block; + vertical-align: top; } #left_column { flex: 79%; margin: 0.2em; + } - h3 { - background: $second-color; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.4em; - margin: 0 0 0.5em 0; - text-transform: uppercase; - font-size: 1.1em; + h3 { + background: $second-color; + box-shadow: $shadow-color 1px 1px 1px; + padding: 0.4em; + margin: 0 0 0.5em 0; + text-transform: uppercase; + font-size: 1.1em; - &:not(:first-of-type) { - margin: 2em 0 1em 0; - } + &:not(:first-of-type) { + margin: 2em 0 1em 0; } } @@ -50,8 +46,8 @@ } } - /* AGENDA/BIRTHDAYS */ - #agenda, + /* LINKS/BIRTHDAYS */ + #links, #birthdays { display: block; width: 100%; @@ -59,9 +55,7 @@ font-size: 70%; margin-bottom: 1em; - #agenda_title, - #birthdays_title { - margin: 0; + h3 { border-radius: 5px 5px 0 0; box-shadow: $shadow-color 1px 1px 1px; padding: 0.5em; @@ -72,34 +66,27 @@ background: $second-color; } - #agenda_content { + #links_content { overflow: auto; box-shadow: $shadow-color 1px 1px 1px; height: 20em; - } - #agenda_content, - #birthdays_content { - .agenda_item { - padding: 0.5em; - margin-bottom: 0.5em; - - &:nth-of-type(even) { - background: $secondary-neutral-light-color; - } - - .agenda_time { - font-size: 90%; - color: grey; - } - - .agenda_item_content { - p { - margin-top: 0.2em; - } - } + h4 { + margin-left: 5px; } + li { + margin: 10px; + } + + i { + width: 20px; + margin: 5px; + } + } + + + #birthdays_content { ul.birthdays_year { margin: 0; list-style-type: none; @@ -124,9 +111,9 @@ } } - /* END AGENDA/BIRTHDAYS */ + /* END AGENDA/BIRTHDAYS */ - /* EVENTS TODAY AND NEXT FEW DAYS */ + /* EVENTS TODAY AND NEXT FEW DAYS */ .news_events_group { box-shadow: $shadow-color 1px 1px 1px; margin-left: 1em; @@ -222,9 +209,9 @@ } } - /* END EVENTS TODAY AND NEXT FEW DAYS */ + /* END EVENTS TODAY AND NEXT FEW DAYS */ - /* COMING SOON */ + /* COMING SOON */ .news_coming_soon { display: list-item; list-style-type: square; @@ -242,9 +229,9 @@ } } - /* END COMING SOON */ + /* END COMING SOON */ - /* NOTICES */ + /* NOTICES */ .news_notice { margin: 0 0 1em 1em; padding: 0.4em; @@ -262,9 +249,9 @@ } } - /* END NOTICES */ + /* END NOTICES */ - /* CALLS */ + /* CALLS */ .news_call { margin: 0 0 1em 1em; padding: 0.4em; @@ -286,7 +273,7 @@ } } - /* END CALLS */ + /* END CALLS */ .news_empty { margin-left: 1em; diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 385cbe32..28f998ca 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -99,35 +99,48 @@ -
-
-
{% trans %}Agenda{% endtrans %}
-
- {% for d in NewsDate.objects.filter(end_date__gte=timezone.now(), - news__is_moderated=True, news__type__in=["WEEKLY", - "EVENT"]).order_by('start_date', 'end_date') %} -
-
- {{ d.start_date|localtime|date('D d M Y') }} -
-
- {{ d.start_date|localtime|time(DATETIME_FORMAT) }} - - {{ d.end_date|localtime|time(DATETIME_FORMAT) }} -
- -
{{ d.news.summary|markdown }}
-
- {% endfor %} +
+
-
{% trans %}Birthdays{% endtrans %}
+

{% trans %}Birthdays{% endtrans %}

- {%- if user.is_subscribed -%} + {%- if user.was_subscribed -%}
    {%- for year, users in birthdays -%}
  • @@ -141,7 +154,7 @@ {%- endfor -%}
{%- else -%} -

{% trans %}You need an up to date subscription to access this content{% endtrans %}

+

{% trans %}You need to subscribe to access this content{% endtrans %}

{%- endif -%}
diff --git a/com/tests.py b/com/tests.py index 399eb0e8..3f98bfdc 100644 --- a/com/tests.py +++ b/com/tests.py @@ -97,9 +97,7 @@ class TestCom(TestCase): response = self.client.get(reverse("core:index")) self.assertContains( response, - text=html.escape( - _("You need an up to date subscription to access this content") - ), + text=html.escape(_("You need to subscribe to access this content")), ) def test_birthday_subscibed_user(self): @@ -107,9 +105,16 @@ class TestCom(TestCase): self.assertNotContains( response, - text=html.escape( - _("You need an up to date subscription to access this content") - ), + text=html.escape(_("You need to subscribe to access this content")), + ) + + def test_birthday_old_subscibed_user(self): + self.client.force_login(User.objects.get(username="old_subscriber")) + response = self.client.get(reverse("core:index")) + + self.assertNotContains( + response, + text=html.escape(_("You need to subscribe to access this content")), ) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index df9689e5..3ae57bdf 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-04 21:59+0100\n" +"POT-Creation-Date: 2025-01-04 23:05+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -356,9 +356,8 @@ msgstr "Nouveau compte club" #: com/templates/com/news_admin_list.jinja com/templates/com/poster_list.jinja #: com/templates/com/screen_list.jinja com/templates/com/weekmail.jinja #: core/templates/core/file.jinja core/templates/core/group_list.jinja -#: core/templates/core/page.jinja core/templates/core/poster_list.jinja -#: core/templates/core/user_tools.jinja core/views/user.py -#: counter/templates/counter/cash_summary_list.jinja +#: core/templates/core/page.jinja core/templates/core/user_tools.jinja +#: core/views/user.py counter/templates/counter/cash_summary_list.jinja #: counter/templates/counter/counter_list.jinja #: election/templates/election/election_detail.jinja #: forum/templates/forum/macros.jinja @@ -1140,7 +1139,7 @@ msgid "New Trombi" msgstr "Nouveau Trombi" #: club/templates/club/club_tools.jinja com/templates/com/poster_list.jinja -#: core/templates/core/poster_list.jinja core/templates/core/user_tools.jinja +#: core/templates/core/user_tools.jinja msgid "Posters" msgstr "Affiches" @@ -1558,17 +1557,46 @@ msgstr "Événements aujourd'hui et dans les prochains jours" msgid "Nothing to come..." msgstr "Rien à venir..." -#: com/templates/com/news_list.jinja -msgid "Coming soon... don't miss!" -msgstr "Prochainement... à ne pas rater!" - #: com/templates/com/news_list.jinja msgid "All coming events" msgstr "Tous les événements à venir" #: com/templates/com/news_list.jinja -msgid "Agenda" -msgstr "Agenda" +msgid "Links" +msgstr "Liens" + +#: com/templates/com/news_list.jinja +msgid "Our services" +msgstr "Nos services" + +#: com/templates/com/news_list.jinja pedagogy/templates/pedagogy/guide.jinja +msgid "UV Guide" +msgstr "Guide des UVs" + +#: com/templates/com/news_list.jinja core/templates/core/base/navbar.jinja +msgid "Matmatronch" +msgstr "Matmatronch" + +#: com/templates/com/news_list.jinja core/templates/core/base/navbar.jinja +#: core/templates/core/user_tools.jinja +msgid "Elections" +msgstr "Élections" + +#: com/templates/com/news_list.jinja +msgid "Social media" +msgstr "Réseaux sociaux" + +#: com/templates/com/news_list.jinja +msgid "Discord" +msgstr "Discord" + +#: com/templates/com/news_list.jinja +msgid "Facebook" +msgstr "Facebook" + +#: com/templates/com/news_list.jinja +msgid "Instagram" +msgstr "Instagram" #: com/templates/com/news_list.jinja msgid "Birthdays" @@ -1580,11 +1608,10 @@ msgid "%(age)s year old" msgstr "%(age)s ans" #: com/templates/com/news_list.jinja com/tests.py -msgid "You need an up to date subscription to access this content" -msgstr "Votre cotisation doit être à jour pour accéder à cette section" +msgid "You need to subscribe to access this content" +msgstr "Vous devez cotiser pour accéder à ce contenu" #: com/templates/com/poster_edit.jinja com/templates/com/poster_list.jinja -#: core/templates/core/poster_list.jinja msgid "Poster" msgstr "Affiche" @@ -1598,15 +1625,15 @@ msgid "Posters - edit" msgstr "Affiche - modifier" #: com/templates/com/poster_list.jinja com/templates/com/screen_list.jinja -#: core/templates/core/poster_list.jinja sas/templates/sas/main.jinja +#: sas/templates/sas/main.jinja msgid "Create" msgstr "Créer" -#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja +#: com/templates/com/poster_list.jinja msgid "Moderation" msgstr "Modération" -#: com/templates/com/poster_list.jinja core/templates/core/poster_list.jinja +#: com/templates/com/poster_list.jinja msgid "No posters" msgstr "Aucune affiche" @@ -2233,10 +2260,6 @@ msgstr "Les clubs de L'AE" msgid "Others UTBM's Associations" msgstr "Les autres associations de l'UTBM" -#: core/templates/core/base/navbar.jinja core/templates/core/user_tools.jinja -msgid "Elections" -msgstr "Élections" - #: core/templates/core/base/navbar.jinja msgid "Big event" msgstr "Grandes Activités" @@ -2264,10 +2287,6 @@ msgstr "Eboutic" msgid "Services" msgstr "Services" -#: core/templates/core/base/navbar.jinja -msgid "Matmatronch" -msgstr "Matmatronch" - #: core/templates/core/base/navbar.jinja launderette/models.py #: launderette/templates/launderette/launderette_book.jinja #: launderette/templates/launderette/launderette_book_choose.jinja @@ -4859,10 +4878,6 @@ msgstr "signalant" msgid "reason" msgstr "raison" -#: pedagogy/templates/pedagogy/guide.jinja -msgid "UV Guide" -msgstr "Guide des UVs" - #: pedagogy/templates/pedagogy/guide.jinja #, python-format msgid "%(display_name)s" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 4c7c5dec..a8b7d40d 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-04 22:00+0100\n" +"POT-Creation-Date: 2025-01-04 23:07+0100\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -17,6 +17,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: com/static/bundled/com/components/ics-calendar-index.ts +msgid "More info" +msgstr "Plus d'informations" + #: core/static/bundled/core/components/ajax-select-base.ts msgid "Remove" msgstr "Retirer" From a13e3e95b7fc1834f107aa481fd7223134a995f0 Mon Sep 17 00:00:00 2001 From: Sli Date: Thu, 2 Jan 2025 02:47:39 +0100 Subject: [PATCH 23/40] Harmonize titles on front page --- com/static/com/css/news-list.scss | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index 4e6ca7a6..dcbad0b5 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -31,7 +31,7 @@ padding: 0.4em; margin: 0 0 0.5em 0; text-transform: uppercase; - font-size: 1.1em; + font-size: 17px; &:not(:first-of-type) { margin: 2em 0 1em 0; @@ -56,14 +56,7 @@ margin-bottom: 1em; h3 { - border-radius: 5px 5px 0 0; - box-shadow: $shadow-color 1px 1px 1px; - padding: 0.5em; - font-weight: bold; - font-size: 150%; - text-align: center; - text-transform: uppercase; - background: $second-color; + margin-bottom: 0; } #links_content { From 007080ee48e84d901f7ddd3a5f820574cd1778ee Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 3 Jan 2025 01:29:19 +0100 Subject: [PATCH 24/40] Extract send_file response creation logic to a dedicated function --- core/views/files.py | 59 ++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/core/views/files.py b/core/views/files.py index f8539080..d5ffabb6 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -13,6 +13,7 @@ # # import mimetypes +from pathlib import Path from urllib.parse import quote, urljoin # This file contains all the views that concern the page model @@ -48,6 +49,41 @@ from core.views.widgets.select import ( from counter.utils import is_logged_in_counter +def send_raw_file(path: Path) -> HttpResponse: + """Send a file located in the MEDIA_ROOT + + This handles all the logic of using production reverse proxy or debug server. + + THIS DOESN'T CHECK ANY PERMISSIONS ! + """ + if not path.is_relative_to(settings.MEDIA_ROOT): + raise Http404 + + if not path.is_file() or not path.exists(): + raise Http404 + + response = HttpResponse( + headers={"Content-Disposition": f'inline; filename="{quote(path.name)}"'} + ) + if not settings.DEBUG: + # When receiving a response with the Accel-Redirect header, + # the reverse proxy will automatically handle the file sending. + # This is really hard to test (thus isn't tested) + # so please do not mess with this. + response["Content-Type"] = "" # automatically set by nginx + response["X-Accel-Redirect"] = quote( + urljoin(settings.MEDIA_URL, str(path.relative_to(settings.MEDIA_ROOT))) + ) + return response + + with open(path, "rb") as filename: + response.content = FileWrapper(filename) + response["Content-Type"] = mimetypes.guess_type(path)[0] + response["Last-Modified"] = http_date(path.stat().st_mtime) + response["Content-Length"] = path.stat().st_size + return response + + def send_file( request: HttpRequest, file_id: int, @@ -66,28 +102,7 @@ def send_file( raise PermissionDenied name = getattr(f, file_attr).name - response = HttpResponse( - headers={"Content-Disposition": f'inline; filename="{quote(name)}"'} - ) - if not settings.DEBUG: - # When receiving a response with the Accel-Redirect header, - # the reverse proxy will automatically handle the file sending. - # This is really hard to test (thus isn't tested) - # so please do not mess with this. - response["Content-Type"] = "" # automatically set by nginx - response["X-Accel-Redirect"] = quote(urljoin(settings.MEDIA_URL, name)) - return response - - filepath = settings.MEDIA_ROOT / name - # check if file exists on disk - if not filepath.exists(): - raise Http404 - with open(filepath, "rb") as filename: - response.content = FileWrapper(filename) - response["Content-Type"] = mimetypes.guess_type(filepath)[0] - response["Last-Modified"] = http_date(f.date.timestamp()) - response["Content-Length"] = filepath.stat().st_size - return response + return send_raw_file(settings.MEDIA_ROOT / name) class MultipleFileInput(forms.ClearableFileInput): From 0a0f44607e2e0db0c1a4dbbcbd7cd0ff2a61af2e Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 3 Jan 2025 01:48:18 +0100 Subject: [PATCH 25/40] Return calendars as real files --- com/api.py | 50 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/com/api.py b/com/api.py index 95e7fe92..871b0eac 100644 --- a/com/api.py +++ b/com/api.py @@ -1,7 +1,8 @@ -from datetime import timedelta +from datetime import datetime, timedelta +from pathlib import Path import urllib3 -from django.core.cache import cache +from django.conf import settings from django.http import HttpResponse from django.urls import reverse from django.utils import timezone @@ -9,30 +10,37 @@ from ics import Calendar, Event from ninja_extra import ControllerBase, api_controller, route from com.models import NewsDate +from core.views.files import send_raw_file @api_controller("/calendar") class CalendarController(ControllerBase): + CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + @route.get("/external.ics") def calendar_external(self): - CACHE_KEY = "external_calendar" - if cached := cache.get(CACHE_KEY): - return HttpResponse( - cached, - content_type="text/calendar", - status=200, - ) + file = self.CACHE_FOLDER / "external.ics" + # Return cached file if updated less than an our ago + if ( + file.exists() + and timezone.make_aware(datetime.fromtimestamp(file.stat().st_mtime)) + + timedelta(hours=1) + > timezone.now() + ): + return send_raw_file(file) + calendar = urllib3.request( "GET", "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", ) - if calendar.status == 200: - cache.set(CACHE_KEY, calendar.data, 3600) # Cache for one hour - return HttpResponse( - calendar.data, - content_type="text/calendar", - status=calendar.status, - ) + if calendar.status != 200: + return HttpResponse(status=calendar.status) + + self.CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(file, "wb") as f: + _ = f.write(calendar.data) + + return send_raw_file(file) @route.get("/internal.ics") def calendar_internal(self): @@ -50,7 +58,9 @@ class CalendarController(ControllerBase): ) calendar.events.add(event) - return HttpResponse( - calendar.serialize().encode("utf-8"), - content_type="text/calendar", - ) + # Create a file so we can offload the download to the reverse proxy if available + file = self.CACHE_FOLDER / "internal.ics" + self.CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(file, "wb") as f: + _ = f.write(calendar.serialize().encode("utf-8")) + return send_raw_file(file) From a60e1f1fdc4a32e8d84cf4ab1d896e9fb413938d Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 3 Jan 2025 13:36:39 +0100 Subject: [PATCH 26/40] Create dedicated class to manage ics calendar files --- com/api.py | 37 +++++++++++++------------------------ com/models.py | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/com/api.py b/com/api.py index 871b0eac..6aec227f 100644 --- a/com/api.py +++ b/com/api.py @@ -1,15 +1,14 @@ -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path -import urllib3 from django.conf import settings -from django.http import HttpResponse +from django.http import Http404 from django.urls import reverse from django.utils import timezone from ics import Calendar, Event from ninja_extra import ControllerBase, api_controller, route -from com.models import NewsDate +from com.models import IcsCalendar, NewsDate from core.views.files import send_raw_file @@ -19,28 +18,18 @@ class CalendarController(ControllerBase): @route.get("/external.ics") def calendar_external(self): - file = self.CACHE_FOLDER / "external.ics" - # Return cached file if updated less than an our ago - if ( - file.exists() - and timezone.make_aware(datetime.fromtimestamp(file.stat().st_mtime)) - + timedelta(hours=1) - > timezone.now() - ): - return send_raw_file(file) + """Return the ICS file of the AE Google Calendar - calendar = urllib3.request( - "GET", - "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", - ) - if calendar.status != 200: - return HttpResponse(status=calendar.status) + Because of Google's cors rules, we can't "just" do a request to google ics + from the frontend. Google is blocking CORS request in it's responses headers. + The only way to do it from the frontend is to use Google Calendar API with an API key + This is not especially desirable as your API key is going to be provided to the frontend. - self.CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(file, "wb") as f: - _ = f.write(calendar.data) - - return send_raw_file(file) + This is why we have this backend based solution. + """ + if (calendar := IcsCalendar.get_external()) is not None: + return send_raw_file(calendar) + raise Http404 @route.get("/internal.ics") def calendar_internal(self): diff --git a/com/models.py b/com/models.py index f3076174..6ec3ce53 100644 --- a/com/models.py +++ b/com/models.py @@ -17,11 +17,16 @@ # details. # # You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple +# this program; if not, write to the Free Software Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # +from datetime import datetime, timedelta +from pathlib import Path +from typing import final + +import urllib3 from django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives @@ -37,6 +42,39 @@ from club.models import Club from core.models import Notification, Preferences, User +@final +class IcsCalendar: + _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" + + @classmethod + def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: + if ( + cls._EXTERNAL_CALENDAR.exists() + and timezone.make_aware( + datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) + ) + + expiration + > timezone.now() + ): + return cls._EXTERNAL_CALENDAR + return cls.make_external() + + @classmethod + def make_external(cls) -> Path | None: + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status != 200: + return None + + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._EXTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.data) + return cls._EXTERNAL_CALENDAR + + class Sith(models.Model): """A one instance class storing all the modifiable infos.""" From 65df55a63520daa3faf8bfa98fc47bb53f0d18b3 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 3 Jan 2025 13:56:40 +0100 Subject: [PATCH 27/40] Use signals to update internal ics --- com/api.py | 27 ++---------------------- com/apps.py | 9 ++++++++ com/models.py | 31 ++++++++++++++++++++++++++++ com/signals.py | 9 ++++++++ core/management/commands/populate.py | 3 ++- 5 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 com/apps.py create mode 100644 com/signals.py diff --git a/com/api.py b/com/api.py index 6aec227f..63a0e680 100644 --- a/com/api.py +++ b/com/api.py @@ -1,14 +1,10 @@ -from datetime import timedelta from pathlib import Path from django.conf import settings from django.http import Http404 -from django.urls import reverse -from django.utils import timezone -from ics import Calendar, Event from ninja_extra import ControllerBase, api_controller, route -from com.models import IcsCalendar, NewsDate +from com.models import IcsCalendar from core.views.files import send_raw_file @@ -33,23 +29,4 @@ class CalendarController(ControllerBase): @route.get("/internal.ics") def calendar_internal(self): - calendar = Calendar() - for news_date in NewsDate.objects.filter( - news__is_moderated=True, - end_date__gte=timezone.now() - - (timedelta(days=30) * 60), # Roughly get the last 6 months - ).prefetch_related("news"): - event = Event( - name=news_date.news.title, - begin=news_date.start_date, - end=news_date.end_date, - url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), - ) - calendar.events.add(event) - - # Create a file so we can offload the download to the reverse proxy if available - file = self.CACHE_FOLDER / "internal.ics" - self.CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(file, "wb") as f: - _ = f.write(calendar.serialize().encode("utf-8")) - return send_raw_file(file) + return send_raw_file(IcsCalendar.get_internal()) diff --git a/com/apps.py b/com/apps.py new file mode 100644 index 00000000..0502c588 --- /dev/null +++ b/com/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ComConfig(AppConfig): + name = "com" + verbose_name = "News and communication" + + def ready(self): + import com.signals # noqa F401 diff --git a/com/models.py b/com/models.py index 6ec3ce53..c7042a38 100644 --- a/com/models.py +++ b/com/models.py @@ -37,6 +37,7 @@ from django.templatetags.static import static from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from ics import Calendar, Event from club.models import Club from core.models import Notification, Preferences, User @@ -46,6 +47,7 @@ from core.models import Notification, Preferences, User class IcsCalendar: _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" + _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" @classmethod def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: @@ -74,6 +76,35 @@ class IcsCalendar: _ = f.write(calendar.data) return cls._EXTERNAL_CALENDAR + @classmethod + def get_internal(cls) -> Path: + if not cls._INTERNAL_CALENDAR.exists(): + return cls.make_internal() + return cls._INTERNAL_CALENDAR + + @classmethod + def make_internal(cls) -> Path: + # Updated through a post_save signal on News in com.signals + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + end_date__gte=timezone.now() + - (timedelta(days=30) * 60), # Roughly get the last 6 months + ).prefetch_related("news"): + event = Event( + name=news_date.news.title, + begin=news_date.start_date, + end=news_date.end_date, + url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), + ) + calendar.events.add(event) + + # Create a file so we can offload the download to the reverse proxy if available + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._INTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.serialize().encode("utf-8")) + return cls._INTERNAL_CALENDAR + class Sith(models.Model): """A one instance class storing all the modifiable infos.""" diff --git a/com/signals.py b/com/signals.py new file mode 100644 index 00000000..b67a4131 --- /dev/null +++ b/com/signals.py @@ -0,0 +1,9 @@ +from django.db.models.base import post_save +from django.dispatch import receiver + +from com.models import IcsCalendar, News + + +@receiver(post_save, sender=News, dispatch_uid="update_internal_ics") +def update_internal_ics(*args, **kwargs): + _ = IcsCalendar.make_internal() diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 222cc509..486f23cd 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -46,7 +46,7 @@ from accounting.models import ( SimplifiedAccountingType, ) from club.models import Club, Membership -from com.models import News, NewsDate, Sith, Weekmail +from com.models import IcsCalendar, News, NewsDate, Sith, Weekmail from core.models import Group, Page, PageRev, SithFile, User from core.utils import resize_image from counter.models import Counter, Product, ProductType, StudentCard @@ -764,6 +764,7 @@ Welcome to the wiki page! ] ) NewsDate.objects.bulk_create(news_dates) + IcsCalendar.make_internal() # Force refresh of the calendar after a bulk_create # Create some data for pedagogy From 5d0fc38107cb01d8a436c71a683c83f57999a528 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 3 Jan 2025 14:38:09 +0100 Subject: [PATCH 28/40] Make social icons links pretty --- com/static/com/css/news-list.scss | 33 ++++++++++++++++++++++++------- com/templates/com/news_list.jinja | 2 +- core/static/core/colors.scss | 2 ++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss index dcbad0b5..bcbf8273 100644 --- a/com/static/com/css/news-list.scss +++ b/com/static/com/css/news-list.scss @@ -68,17 +68,36 @@ margin-left: 5px; } - li { - margin: 10px; + ul { + list-style: none; + margin-left: 0; + + li { + margin: 10px; + + .fa-facebook { + color: $faceblue; + } + + .fa-discord { + color: $discordblurple; + } + + .fa-square-instagram::before { + background: $instagradient; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + i { + width: 25px; + text-align: center; + } + } } - i { - width: 20px; - margin: 5px; - } } - #birthdays_content { ul.birthdays_year { margin: 0; diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 28f998ca..8f20ce19 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -130,7 +130,7 @@ {% trans %}Facebook{% endtrans %}
  • - + {% trans %}Instagram{% endtrans %}
  • diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss index 35dc6a69..e10eb905 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -24,6 +24,8 @@ $black-color: hsl(0, 0%, 17%); $faceblue: hsl(221, 44%, 41%); $twitblue: hsl(206, 82%, 63%); +$discordblurple: #7289da; +$instagradient: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); $shadow-color: rgb(223, 223, 223); From 1887a2790f32b794a6b322c01a5ff8cc2ef4f15c Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 4 Jan 2025 18:57:31 +0100 Subject: [PATCH 29/40] Move IcsCalendar to it's own file --- com/api.py | 4 +- com/calendar.py | 74 ++++++++++++++++++++++++++++ com/models.py | 68 ------------------------- com/signals.py | 3 +- core/management/commands/populate.py | 3 +- 5 files changed, 80 insertions(+), 72 deletions(-) create mode 100644 com/calendar.py diff --git a/com/api.py b/com/api.py index 63a0e680..9a5b1398 100644 --- a/com/api.py +++ b/com/api.py @@ -4,7 +4,7 @@ from django.conf import settings from django.http import Http404 from ninja_extra import ControllerBase, api_controller, route -from com.models import IcsCalendar +from com.calendar import IcsCalendar from core.views.files import send_raw_file @@ -16,7 +16,7 @@ class CalendarController(ControllerBase): def calendar_external(self): """Return the ICS file of the AE Google Calendar - Because of Google's cors rules, we can't "just" do a request to google ics + Because of Google's cors rules, we can't just do a request to google ics from the frontend. Google is blocking CORS request in it's responses headers. The only way to do it from the frontend is to use Google Calendar API with an API key This is not especially desirable as your API key is going to be provided to the frontend. diff --git a/com/calendar.py b/com/calendar.py new file mode 100644 index 00000000..52cb25b9 --- /dev/null +++ b/com/calendar.py @@ -0,0 +1,74 @@ +from datetime import datetime, timedelta +from pathlib import Path +from typing import final + +import urllib3 +from django.conf import settings +from django.urls import reverse +from django.utils import timezone +from ics import Calendar, Event + +from com.models import NewsDate + + +@final +class IcsCalendar: + _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" + _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" + _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" + + @classmethod + def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: + if ( + cls._EXTERNAL_CALENDAR.exists() + and timezone.make_aware( + datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) + ) + + expiration + > timezone.now() + ): + return cls._EXTERNAL_CALENDAR + return cls.make_external() + + @classmethod + def make_external(cls) -> Path | None: + calendar = urllib3.request( + "GET", + "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", + ) + if calendar.status != 200: + return None + + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._EXTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.data) + return cls._EXTERNAL_CALENDAR + + @classmethod + def get_internal(cls) -> Path: + if not cls._INTERNAL_CALENDAR.exists(): + return cls.make_internal() + return cls._INTERNAL_CALENDAR + + @classmethod + def make_internal(cls) -> Path: + # Updated through a post_save signal on News in com.signals + calendar = Calendar() + for news_date in NewsDate.objects.filter( + news__is_moderated=True, + end_date__gte=timezone.now() + - (timedelta(days=30) * 60), # Roughly get the last 6 months + ).prefetch_related("news"): + event = Event( + name=news_date.news.title, + begin=news_date.start_date, + end=news_date.end_date, + url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), + ) + calendar.events.add(event) + + # Create a file so we can offload the download to the reverse proxy if available + cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) + with open(cls._INTERNAL_CALENDAR, "wb") as f: + _ = f.write(calendar.serialize().encode("utf-8")) + return cls._INTERNAL_CALENDAR diff --git a/com/models.py b/com/models.py index c7042a38..633c7671 100644 --- a/com/models.py +++ b/com/models.py @@ -22,11 +22,7 @@ # # -from datetime import datetime, timedelta -from pathlib import Path -from typing import final -import urllib3 from django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives @@ -37,75 +33,11 @@ from django.templatetags.static import static from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from ics import Calendar, Event from club.models import Club from core.models import Notification, Preferences, User -@final -class IcsCalendar: - _CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" - _EXTERNAL_CALENDAR = _CACHE_FOLDER / "external.ics" - _INTERNAL_CALENDAR = _CACHE_FOLDER / "internal.ics" - - @classmethod - def get_external(cls, expiration: timedelta = timedelta(hours=1)) -> Path | None: - if ( - cls._EXTERNAL_CALENDAR.exists() - and timezone.make_aware( - datetime.fromtimestamp(cls._EXTERNAL_CALENDAR.stat().st_mtime) - ) - + expiration - > timezone.now() - ): - return cls._EXTERNAL_CALENDAR - return cls.make_external() - - @classmethod - def make_external(cls) -> Path | None: - calendar = urllib3.request( - "GET", - "https://calendar.google.com/calendar/ical/ae.utbm%40gmail.com/public/basic.ics", - ) - if calendar.status != 200: - return None - - cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(cls._EXTERNAL_CALENDAR, "wb") as f: - _ = f.write(calendar.data) - return cls._EXTERNAL_CALENDAR - - @classmethod - def get_internal(cls) -> Path: - if not cls._INTERNAL_CALENDAR.exists(): - return cls.make_internal() - return cls._INTERNAL_CALENDAR - - @classmethod - def make_internal(cls) -> Path: - # Updated through a post_save signal on News in com.signals - calendar = Calendar() - for news_date in NewsDate.objects.filter( - news__is_moderated=True, - end_date__gte=timezone.now() - - (timedelta(days=30) * 60), # Roughly get the last 6 months - ).prefetch_related("news"): - event = Event( - name=news_date.news.title, - begin=news_date.start_date, - end=news_date.end_date, - url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), - ) - calendar.events.add(event) - - # Create a file so we can offload the download to the reverse proxy if available - cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) - with open(cls._INTERNAL_CALENDAR, "wb") as f: - _ = f.write(calendar.serialize().encode("utf-8")) - return cls._INTERNAL_CALENDAR - - class Sith(models.Model): """A one instance class storing all the modifiable infos.""" diff --git a/com/signals.py b/com/signals.py index b67a4131..ea004ad8 100644 --- a/com/signals.py +++ b/com/signals.py @@ -1,7 +1,8 @@ from django.db.models.base import post_save from django.dispatch import receiver -from com.models import IcsCalendar, News +from com.calendar import IcsCalendar +from com.models import News @receiver(post_save, sender=News, dispatch_uid="update_internal_ics") diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 486f23cd..3ed1025d 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -46,7 +46,8 @@ from accounting.models import ( SimplifiedAccountingType, ) from club.models import Club, Membership -from com.models import IcsCalendar, News, NewsDate, Sith, Weekmail +from com.calendar import IcsCalendar +from com.models import News, NewsDate, Sith, Weekmail from core.models import Group, Page, PageRev, SithFile, User from core.utils import resize_image from counter.models import Counter, Product, ProductType, StudentCard From ba76015c71957a2a9a47e3671f0f369064e6d8d1 Mon Sep 17 00:00:00 2001 From: Sli Date: Sat, 4 Jan 2025 19:24:40 +0100 Subject: [PATCH 30/40] Use a newer ical library --- com/calendar.py | 16 ++++---- poetry.lock | 101 ++++++++++++------------------------------------ pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 84 deletions(-) diff --git a/com/calendar.py b/com/calendar.py index 52cb25b9..9003d6de 100644 --- a/com/calendar.py +++ b/com/calendar.py @@ -3,10 +3,13 @@ from pathlib import Path from typing import final import urllib3 +from dateutil.relativedelta import relativedelta from django.conf import settings from django.urls import reverse from django.utils import timezone -from ics import Calendar, Event +from ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.event import Event from com.models import NewsDate @@ -56,19 +59,18 @@ class IcsCalendar: calendar = Calendar() for news_date in NewsDate.objects.filter( news__is_moderated=True, - end_date__gte=timezone.now() - - (timedelta(days=30) * 60), # Roughly get the last 6 months + end_date__gte=timezone.now() - (relativedelta(months=6)), ).prefetch_related("news"): event = Event( - name=news_date.news.title, - begin=news_date.start_date, + summary=news_date.news.title, + start=news_date.start_date, end=news_date.end_date, url=reverse("com:news_detail", kwargs={"news_id": news_date.news.id}), ) - calendar.events.add(event) + calendar.events.append(event) # Create a file so we can offload the download to the reverse proxy if available cls._CACHE_FOLDER.mkdir(parents=True, exist_ok=True) with open(cls._INTERNAL_CALENDAR, "wb") as f: - _ = f.write(calendar.serialize().encode("utf-8")) + _ = f.write(IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")) return cls._INTERNAL_CALENDAR diff --git a/poetry.lock b/poetry.lock index 3c4c08b0..695b4503 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,25 +22,6 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - [[package]] name = "asgiref" version = "3.8.1" @@ -70,25 +51,6 @@ files = [ astroid = ["astroid (>=2,<4)"] test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] -[[package]] -name = "attrs" -version = "24.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -files = [ - {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, - {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - [[package]] name = "babel" version = "2.16.0" @@ -970,22 +932,21 @@ files = [ ] [[package]] -name = "ics" -version = "0.7.2" -description = "Python icalendar (rfc5545) parser" +name = "ical" +version = "8.3.0" +description = "Python iCalendar implementation (rfc 2445)" optional = false -python-versions = "*" +python-versions = ">=3.10" files = [ - {file = "ics-0.7.2-py2.py3-none-any.whl", hash = "sha256:5fcf4d29ec6e7dfcb84120abd617bbba632eb77b097722b7df70e48dbcf26103"}, - {file = "ics-0.7.2.tar.gz", hash = "sha256:6743539bca10391635249b87d74fcd1094af20b82098bebf7c7521df91209f05"}, + {file = "ical-8.3.0-py3-none-any.whl", hash = "sha256:606f2f561bd8b75cb726710dddbb20f3f84dfa1d6323550947dba97359423850"}, + {file = "ical-8.3.0.tar.gz", hash = "sha256:e277cc518cbb0132e6827c318c8ec3b379b125ebf0a2a44337f08795d5530937"}, ] [package.dependencies] -arrow = ">=0.11" -attrs = ">=19.1.0" -python-dateutil = "*" -six = ">1.5" -tatsu = ">4.2" +pydantic = ">=1.9.1" +pyparsing = ">=3.0.9" +python-dateutil = ">=2.8.2" +tzdata = ">=2023.3" [[package]] name = "identify" @@ -1939,6 +1900,20 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pyparsing" +version = "3.2.1" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pytest" version = "8.3.4" @@ -2580,21 +2555,6 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] -[[package]] -name = "tatsu" -version = "5.12.2" -description = "TatSu takes a grammar in a variation of EBNF as input, and outputs a memoizing PEG/Packrat parser in Python." -optional = false -python-versions = ">=3.11" -files = [ - {file = "TatSu-5.12.2-py3-none-any.whl", hash = "sha256:9c313186ae5262662cb3fbec52c9a12db1ef752e615f46cac3eb568cb91eacf9"}, - {file = "tatsu-5.12.2.tar.gz", hash = "sha256:5894dc7ddba9a1886a95ff2f06cef1be2b3d3a37c776eba8177ef4dcd80ccb03"}, -] - -[package.extras] -colorization = ["colorama"] -parproc = ["rich"] - [[package]] name = "tomli" version = "2.2.1" @@ -2651,17 +2611,6 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -2806,4 +2755,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f0acbbe66fd99ac04891bcc8a5f28167a927e0b1f3677ebd8ab302a0e2fb9be2" +content-hash = "7f348f74a05c27e29aaaf25a5584bba9b416f42c3f370db234dd69e5e10dc8df" diff --git a/pyproject.toml b/pyproject.toml index f3427faf..3d761bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Sphinx = "^5" # Needed for building xapian tomli = "^2.2.1" django-honeypot = "^1.2.1" pydantic-extra-types = "^2.10.1" -ics = "^0.7.2" +ical = "^8.3.0" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development From fa7f5d24b014af0bc0ddce45505e3960ec295c35 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 5 Jan 2025 01:04:11 +0100 Subject: [PATCH 31/40] Test external calendar api --- com/api.py | 4 +- com/tests/test_api.py | 54 +++++++++++++++++++++++++++ com/{tests.py => tests/test_views.py} | 0 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 com/tests/test_api.py rename com/{tests.py => tests/test_views.py} (100%) diff --git a/com/api.py b/com/api.py index 9a5b1398..e46daea9 100644 --- a/com/api.py +++ b/com/api.py @@ -12,7 +12,7 @@ from core.views.files import send_raw_file class CalendarController(ControllerBase): CACHE_FOLDER: Path = settings.MEDIA_ROOT / "com" / "calendars" - @route.get("/external.ics") + @route.get("/external.ics", url_name="calendar_external") def calendar_external(self): """Return the ICS file of the AE Google Calendar @@ -27,6 +27,6 @@ class CalendarController(ControllerBase): return send_raw_file(calendar) raise Http404 - @route.get("/internal.ics") + @route.get("/internal.ics", url_name="calendar_internal") def calendar_internal(self): return send_raw_file(IcsCalendar.get_internal()) diff --git a/com/tests/test_api.py b/com/tests/test_api.py new file mode 100644 index 00000000..0a8f3f96 --- /dev/null +++ b/com/tests/test_api.py @@ -0,0 +1,54 @@ +from dataclasses import dataclass +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from django.conf import settings +from django.test.client import Client +from django.urls import reverse + +from com.calendar import IcsCalendar + + +@dataclass +class MockResponse: + status: int + value: str + + @property + def data(self): + return self.value.encode("utf8") + + +@pytest.mark.django_db +class TestExternalCalendar: + @pytest.fixture + def mock_request(self): + request = MagicMock() + with patch("urllib3.request", request): + yield request + + @pytest.fixture(autouse=True) + def clear_cache(self): + IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True) + + @pytest.mark.parametrize("error_code", [403, 404, 500]) + def test_fetch_error( + self, client: Client, mock_request: MagicMock, error_code: int + ): + mock_request.return_value = MockResponse(error_code, "not allowed") + assert client.get(reverse("api:calendar_external")).status_code == 404 + + def test_fetch_success(self, client: Client, mock_request: MagicMock): + external_response = MockResponse(200, "Definitely an ICS") + mock_request.return_value = external_response + response = client.get(reverse("api:calendar_external")) + assert response.status_code == 200 + redirect = Path(response.headers.get("X-Accel-Redirect", "")) + assert redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem) + out_file = settings.MEDIA_ROOT / redirect.relative_to( + Path("/") / settings.MEDIA_ROOT.stem + ) + assert out_file.exists() + with open(out_file, "r") as f: + assert f.read() == external_response.value diff --git a/com/tests.py b/com/tests/test_views.py similarity index 100% rename from com/tests.py rename to com/tests/test_views.py From eb3db134f86a9e30fb68cc523d9797292be26d8c Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 5 Jan 2025 01:32:54 +0100 Subject: [PATCH 32/40] Test external calendar caching --- com/tests/test_api.py | 70 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/com/tests/test_api.py b/com/tests/test_api.py index 0a8f3f96..cfc0e35f 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -1,11 +1,15 @@ from dataclasses import dataclass +from datetime import datetime, timedelta from pathlib import Path +from typing import Callable from unittest.mock import MagicMock, patch import pytest from django.conf import settings +from django.http import HttpResponse from django.test.client import Client from django.urls import reverse +from django.utils import timezone from com.calendar import IcsCalendar @@ -20,13 +24,29 @@ class MockResponse: return self.value.encode("utf8") +def accel_redirect_to_file(response: HttpResponse) -> Path | None: + redirect = Path(response.headers.get("X-Accel-Redirect", "")) + if not redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem): + return None + return settings.MEDIA_ROOT / redirect.relative_to( + Path("/") / settings.MEDIA_ROOT.stem + ) + + @pytest.mark.django_db class TestExternalCalendar: @pytest.fixture def mock_request(self): - request = MagicMock() - with patch("urllib3.request", request): - yield request + mock = MagicMock() + with patch("urllib3.request", mock): + yield mock + + @pytest.fixture + def mock_current_time(self): + mock = MagicMock() + original = timezone.now + with patch("django.utils.timezone.now", mock): + yield mock, original @pytest.fixture(autouse=True) def clear_cache(self): @@ -44,11 +64,45 @@ class TestExternalCalendar: mock_request.return_value = external_response response = client.get(reverse("api:calendar_external")) assert response.status_code == 200 - redirect = Path(response.headers.get("X-Accel-Redirect", "")) - assert redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem) - out_file = settings.MEDIA_ROOT / redirect.relative_to( - Path("/") / settings.MEDIA_ROOT.stem - ) + out_file = accel_redirect_to_file(response) + assert out_file is not None assert out_file.exists() with open(out_file, "r") as f: assert f.read() == external_response.value + + def test_fetch_caching( + self, + client: Client, + mock_request: MagicMock, + mock_current_time: tuple[MagicMock, Callable[[], datetime]], + ): + fake_current_time, original_timezone = mock_current_time + start_time = original_timezone() + + fake_current_time.return_value = start_time + external_response = MockResponse(200, "Definitely an ICS") + mock_request.return_value = external_response + + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + mock_request.return_value = MockResponse(200, "This should be ignored") + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + mock_request.assert_called_once() + + fake_current_time.return_value = start_time + timedelta(hours=1, seconds=1) + external_response = MockResponse(200, "This won't be ignored") + mock_request.return_value = external_response + + with open( + accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r" + ) as f: + assert f.read() == external_response.value + + assert mock_request.call_count == 2 From 2749a88704b11d48c4e33d19f5e198b8aadcec8b Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 5 Jan 2025 01:36:41 +0100 Subject: [PATCH 33/40] Basic test for internal calendar --- com/tests/test_api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/com/tests/test_api.py b/com/tests/test_api.py index cfc0e35f..f131052e 100644 --- a/com/tests/test_api.py +++ b/com/tests/test_api.py @@ -106,3 +106,17 @@ class TestExternalCalendar: assert f.read() == external_response.value assert mock_request.call_count == 2 + + +@pytest.mark.django_db +class TestInternalCalendar: + @pytest.fixture(autouse=True) + def clear_cache(self): + IcsCalendar._INTERNAL_CALENDAR.unlink(missing_ok=True) + + def test_fetch_success(self, client: Client): + response = client.get(reverse("api:calendar_internal")) + assert response.status_code == 200 + out_file = accel_redirect_to_file(response) + assert out_file is not None + assert out_file.exists() From 785ac9bdab6dd3ac969139ec00fab5a278973b30 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 5 Jan 2025 14:48:40 +0100 Subject: [PATCH 34/40] pin poetry version --- .github/actions/setup_project/action.yml | 2 +- docs/tutorial/install.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup_project/action.yml b/.github/actions/setup_project/action.yml index 599fa11e..2e590471 100644 --- a/.github/actions/setup_project/action.yml +++ b/.github/actions/setup_project/action.yml @@ -24,7 +24,7 @@ runs: - name: Install Poetry if: steps.cached-poetry.outputs.cache-hit != 'true' shell: bash - run: curl -sSL https://install.python-poetry.org | python3 - + run: curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5 - name: Check pyproject.toml syntax shell: bash diff --git a/docs/tutorial/install.md b/docs/tutorial/install.md index c3b0756e..d59dfee0 100644 --- a/docs/tutorial/install.md +++ b/docs/tutorial/install.md @@ -81,7 +81,7 @@ cd /mnt//vos/fichiers/comme/dhab libffi-dev python-dev-is-python3 pkg-config \ gettext git pipx - pipx install poetry + pipx install poetry==1.8.5 ``` === "Arch Linux" @@ -101,7 +101,7 @@ cd /mnt//vos/fichiers/comme/dhab ```bash brew install git python pipx npm - pipx install poetry + pipx install poetry==1.8.5 # Pour bien configurer gettext brew link gettext # (suivez bien les instructions supplémentaires affichées) From 348ab19ac6dc1837e49e8c072c96d1e65cbdf88e Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 5 Jan 2025 15:40:41 +0100 Subject: [PATCH 35/40] small form fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit le `display:block` avait disparu des helptext, ce qui rendait leur affichage bizarre. Et il manquait quelques détails sur le `ProductForm` --- com/static/com/css/news-detail.scss | 5 ----- core/static/core/forms.scss | 1 + counter/forms.py | 9 +++++++++ locale/fr/LC_MESSAGES/django.po | 12 ++++++++++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss index 0a07e62d..c0d633fb 100644 --- a/com/static/com/css/news-detail.scss +++ b/com/static/com/css/news-detail.scss @@ -59,8 +59,3 @@ font-size: small; } } - -.helptext { - margin-top: 10px; - display: block; -} \ No newline at end of file diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss index ae6f8a21..dd44aa8a 100644 --- a/core/static/core/forms.scss +++ b/core/static/core/forms.scss @@ -145,6 +145,7 @@ form { margin-top: .25rem; margin-bottom: .25rem; font-size: 80%; + display: block; } fieldset { diff --git a/counter/forms.py b/counter/forms.py index d422e9ac..59762920 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -154,6 +154,9 @@ class CounterEditForm(forms.ModelForm): class ProductEditForm(forms.ModelForm): + error_css_class = "error" + required_css_class = "required" + class Meta: model = Product fields = [ @@ -171,6 +174,12 @@ class ProductEditForm(forms.ModelForm): "tray", "archived", ] + help_texts = { + "description": _( + "Describe the product. If it's an event's click, " + "give some insights about it, like the date (including the year)." + ) + } widgets = { "product_type": AutoCompleteSelect, "buying_groups": AutoCompleteSelectMultipleGroup, diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 3ae57bdf..8540b562 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-04 23:05+0100\n" +"POT-Creation-Date: 2025-01-05 15:28+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -1607,7 +1607,7 @@ msgstr "Anniversaires" msgid "%(age)s year old" msgstr "%(age)s ans" -#: com/templates/com/news_list.jinja com/tests.py +#: com/templates/com/news_list.jinja com/tests/test_views.py msgid "You need to subscribe to access this content" msgstr "Vous devez cotiser pour accéder à ce contenu" @@ -3364,6 +3364,14 @@ msgstr "Cet UID est invalide" msgid "User not found" msgstr "Utilisateur non trouvé" +#: counter/forms.py +msgid "" +"Describe the product. If it's an event's click, give some insights about it, " +"like the date (including the year)." +msgstr "" +"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques détails " +"dessus, comme la date (en incluant l'année)." + #: counter/management/commands/dump_accounts.py msgid "Your AE account has been emptied" msgstr "Votre compte AE a été vidé" From af47587116606b30ff407d57ae2ebdc4a417259b Mon Sep 17 00:00:00 2001 From: imperosol Date: Tue, 31 Dec 2024 16:09:08 +0100 Subject: [PATCH 36/40] Split groups and ban groups --- core/admin.py | 22 ++- core/management/commands/populate.py | 13 +- ..._description_alter_user_groups_and_more.py | 164 ++++++++++++++++++ core/models.py | 89 +++++++++- core/tests/test_core.py | 4 - counter/tests/test_counter.py | 10 +- sith/settings.py | 13 +- 7 files changed, 285 insertions(+), 30 deletions(-) create mode 100644 core/migrations/0043_bangroup_alter_group_description_alter_user_groups_and_more.py diff --git a/core/admin.py b/core/admin.py index 601ba636..5de89ada 100644 --- a/core/admin.py +++ b/core/admin.py @@ -17,7 +17,7 @@ from django.contrib import admin from django.contrib.auth.models import Group as AuthGroup from django.contrib.auth.models import Permission -from core.models import Group, OperationLog, Page, SithFile, User +from core.models import BanGroup, Group, OperationLog, Page, SithFile, User, UserBan admin.site.unregister(AuthGroup) @@ -30,6 +30,19 @@ class GroupAdmin(admin.ModelAdmin): autocomplete_fields = ("permissions",) +@admin.register(BanGroup) +class BanGroupAdmin(admin.ModelAdmin): + list_display = ("name", "description") + search_fields = ("name",) + autocomplete_fields = ("permissions",) + + +class UserBanInline(admin.TabularInline): + model = UserBan + extra = 0 + autocomplete_fields = ("ban_group",) + + @admin.register(User) class UserAdmin(admin.ModelAdmin): list_display = ("first_name", "last_name", "username", "email", "nick_name") @@ -42,9 +55,16 @@ class UserAdmin(admin.ModelAdmin): "user_permissions", "groups", ) + inlines = (UserBanInline,) search_fields = ["first_name", "last_name", "username"] +@admin.register(UserBan) +class UserBanAdmin(admin.ModelAdmin): + list_display = ("user", "ban_group", "created_at", "expires_at") + autocomplete_fields = ("user", "ban_group") + + @admin.register(Permission) class PermissionAdmin(admin.ModelAdmin): search_fields = ("codename",) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 3ed1025d..e3d6d8e4 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -48,7 +48,7 @@ from accounting.models import ( from club.models import Club, Membership from com.calendar import IcsCalendar from com.models import News, NewsDate, Sith, Weekmail -from core.models import Group, Page, PageRev, SithFile, User +from core.models import BanGroup, Group, Page, PageRev, SithFile, User from core.utils import resize_image from counter.models import Counter, Product, ProductType, StudentCard from election.models import Candidature, Election, ElectionList, Role @@ -94,6 +94,7 @@ class Command(BaseCommand): Sith.objects.create(weekmail_destinations="etudiants@git.an personnel@git.an") Site.objects.create(domain=settings.SITH_URL, name=settings.SITH_NAME) groups = self._create_groups() + self._create_ban_groups() root = User.objects.create_superuser( id=0, @@ -951,11 +952,6 @@ Welcome to the wiki page! ) ) ) - Group.objects.create( - name="Banned from buying alcohol", is_manually_manageable=True - ) - Group.objects.create(name="Banned from counters", is_manually_manageable=True) - Group.objects.create(name="Banned to subscribe", is_manually_manageable=True) sas_admin = Group.objects.create(name="SAS admin", is_manually_manageable=True) sas_admin.permissions.add( *list( @@ -995,3 +991,8 @@ Welcome to the wiki page! sas_admin=sas_admin, pedagogy_admin=pedagogy_admin, ) + + def _create_ban_groups(self): + BanGroup.objects.create(name="Banned from buying alcohol", description="") + BanGroup.objects.create(name="Banned from counters", description="") + BanGroup.objects.create(name="Banned to subscribe", description="") diff --git a/core/migrations/0043_bangroup_alter_group_description_alter_user_groups_and_more.py b/core/migrations/0043_bangroup_alter_group_description_alter_user_groups_and_more.py new file mode 100644 index 00000000..daba4097 --- /dev/null +++ b/core/migrations/0043_bangroup_alter_group_description_alter_user_groups_and_more.py @@ -0,0 +1,164 @@ +# Generated by Django 4.2.17 on 2024-12-31 13:30 + +import django.contrib.auth.models +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models +from django.db.migrations.state import StateApps + + +def migrate_ban_groups(apps: StateApps, schema_editor): + Group = apps.get_model("core", "Group") + BanGroup = apps.get_model("core", "BanGroup") + ban_group_ids = [ + settings.SITH_GROUP_BANNED_ALCOHOL_ID, + settings.SITH_GROUP_BANNED_COUNTER_ID, + settings.SITH_GROUP_BANNED_SUBSCRIPTION_ID, + ] + # this is a N+1 Queries, but the prod database has a grand total of 3 ban groups + for group in Group.objects.filter(id__in=ban_group_ids): + # auth_group, which both Group and BanGroup inherit, + # is unique by name. + # If we tried give the exact same name to the migrated BanGroup + # before deleting the corresponding Group, + # we would have an IntegrityError. + # So we append a space to the name, in order to create a name + # that will look the same, but that isn't really the same. + ban_group = BanGroup.objects.create( + name=f"{group.name} ", + description=group.description, + ) + perms = list(group.permissions.values_list("id", flat=True)) + if perms: + ban_group.permissions.add(*perms) + ban_group.users.add( + *group.users.values_list("id", flat=True), through_defaults={"reason": ""} + ) + group.delete() + # now that the original group is no longer there, + # we can remove the appended space + ban_group.name = ban_group.name.strip() + ban_group.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("core", "0042_invert_is_manually_manageable_20250104_1742"), + ] + + operations = [ + migrations.CreateModel( + name="BanGroup", + fields=[ + ( + "group_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="auth.group", + ), + ), + ("description", models.TextField(verbose_name="description")), + ], + bases=("auth.group",), + managers=[ + ("objects", django.contrib.auth.models.GroupManager()), + ], + options={ + "verbose_name": "ban group", + "verbose_name_plural": "ban groups", + }, + ), + migrations.AlterField( + model_name="group", + name="description", + field=models.TextField(verbose_name="description"), + ), + migrations.AlterField( + model_name="user", + name="groups", + field=models.ManyToManyField( + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="users", + to="core.group", + verbose_name="groups", + ), + ), + migrations.CreateModel( + name="UserBan", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="created at"), + ), + ( + "expires_at", + models.DateTimeField( + blank=True, + help_text="When the ban should be removed. Currently, there is no automatic removal, so this is purely indicative. Automatic ban removal may be implemented later on.", + null=True, + verbose_name="expires at", + ), + ), + ("reason", models.TextField(verbose_name="reason")), + ( + "ban_group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_bans", + to="core.bangroup", + verbose_name="ban type", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="bans", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), + ], + ), + migrations.AddField( + model_name="user", + name="ban_groups", + field=models.ManyToManyField( + help_text="The bans this user has received.", + related_name="users", + through="core.UserBan", + to="core.bangroup", + verbose_name="ban groups", + ), + ), + migrations.AddConstraint( + model_name="userban", + constraint=models.UniqueConstraint( + fields=("ban_group", "user"), name="unique_ban_type_per_user" + ), + ), + migrations.AddConstraint( + model_name="userban", + constraint=models.CheckConstraint( + check=models.Q(("expires_at__gte", models.F("created_at"))), + name="user_ban_end_after_start", + ), + ), + migrations.RunPython( + migrate_ban_groups, reverse_code=migrations.RunPython.noop, elidable=True + ), + ] diff --git a/core/models.py b/core/models.py index 945fd7d0..278182ac 100644 --- a/core/models.py +++ b/core/models.py @@ -42,7 +42,7 @@ from django.core.cache import cache from django.core.exceptions import PermissionDenied, ValidationError from django.core.mail import send_mail from django.db import models, transaction -from django.db.models import Exists, OuterRef, Q +from django.db.models import Exists, F, OuterRef, Q from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -65,8 +65,7 @@ class Group(AuthGroup): default=False, help_text=_("If False, this shouldn't be shown on group management pages"), ) - #: Description of the group - description = models.CharField(_("description"), max_length=60) + description = models.TextField(_("description")) def get_absolute_url(self) -> str: return reverse("core:group_list") @@ -134,6 +133,28 @@ def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None return group +class BanGroup(AuthGroup): + """An anti-group, that removes permissions instead of giving them. + + Users are linked to BanGroups through UserBan objects. + + Example: + ```python + user = User.objects.get(username="...") + ban_group = BanGroup.objects.first() + UserBan.objects.create(user=user, ban_group=ban_group, reason="...") + + assert user.ban_groups.contains(ban_group) + ``` + """ + + description = models.TextField(_("description")) + + class Meta: + verbose_name = _("ban group") + verbose_name_plural = _("ban groups") + + class UserQuerySet(models.QuerySet): def filter_inactive(self) -> Self: from counter.models import Refilling, Selling @@ -184,7 +205,13 @@ class User(AbstractUser): "granted to each of their groups." ), related_name="users", - blank=True, + ) + ban_groups = models.ManyToManyField( + BanGroup, + verbose_name=_("ban groups"), + through="UserBan", + help_text=_("The bans this user has received."), + related_name="users", ) home = models.OneToOneField( "SithFile", @@ -424,12 +451,12 @@ class User(AbstractUser): ) @cached_property - def is_banned_alcohol(self): - return self.is_in_group(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID) + def is_banned_alcohol(self) -> bool: + return self.ban_groups.filter(id=settings.SITH_GROUP_BANNED_ALCOHOL_ID).exists() @cached_property - def is_banned_counter(self): - return self.is_in_group(pk=settings.SITH_GROUP_BANNED_COUNTER_ID) + def is_banned_counter(self) -> bool: + return self.ban_groups.filter(id=settings.SITH_GROUP_BANNED_COUNTER_ID).exists() @cached_property def age(self) -> int: @@ -731,6 +758,52 @@ class AnonymousUser(AuthAnonymousUser): return _("Visitor") +class UserBan(models.Model): + """A ban of a user. + + A user can be banned for a specific reason, for a specific duration. + The expiration date is indicative, and the ban should be removed manually. + """ + + ban_group = models.ForeignKey( + BanGroup, + verbose_name=_("ban type"), + related_name="user_bans", + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + User, verbose_name=_("user"), related_name="bans", on_delete=models.CASCADE + ) + created_at = models.DateTimeField(_("created at"), auto_now_add=True) + expires_at = models.DateTimeField( + _("expires at"), + null=True, + blank=True, + help_text=_( + "When the ban should be removed. " + "Currently, there is no automatic removal, so this is purely indicative. " + "Automatic ban removal may be implemented later on." + ), + ) + reason = models.TextField(_("reason")) + + class Meta: + verbose_name = _("user ban") + verbose_name_plural = _("user bans") + constraints = [ + models.UniqueConstraint( + fields=["ban_group", "user"], name="unique_ban_type_per_user" + ), + models.CheckConstraint( + check=Q(expires_at__gte=F("created_at")), + name="user_ban_end_after_start", + ), + ] + + def __str__(self): + return f"Ban of user {self.user.id}" + + class Preferences(models.Model): user = models.OneToOneField( User, related_name="_preferences", on_delete=models.CASCADE diff --git a/core/tests/test_core.py b/core/tests/test_core.py index a152b579..878db4e4 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -358,9 +358,6 @@ class TestUserIsInGroup(TestCase): cls.accounting_admin = Group.objects.get(name="Accounting admin") cls.com_admin = Group.objects.get(name="Communication admin") cls.counter_admin = Group.objects.get(name="Counter admin") - cls.banned_alcohol = Group.objects.get(name="Banned from buying alcohol") - cls.banned_counters = Group.objects.get(name="Banned from counters") - cls.banned_subscription = Group.objects.get(name="Banned to subscribe") cls.sas_admin = Group.objects.get(name="SAS admin") cls.club = baker.make(Club) cls.main_club = Club.objects.get(id=1) @@ -373,7 +370,6 @@ class TestUserIsInGroup(TestCase): self.assert_in_public_group(user) for group in ( self.root_group, - self.banned_counters, self.accounting_admin, self.sas_admin, self.subscribers, diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 1b378d5b..3fdc7099 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -31,7 +31,7 @@ from model_bakery import baker from club.models import Club, Membership from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user -from core.models import Group, User +from core.models import BanGroup, User from counter.baker_recipes import product_recipe from counter.models import ( Counter, @@ -229,11 +229,11 @@ class TestCounterClick(TestFullClickBase): cls.set_age(cls.banned_alcohol_customer, 20) cls.set_age(cls.underage_customer, 17) - cls.banned_alcohol_customer.groups.add( - Group.objects.get(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID) + cls.banned_alcohol_customer.ban_groups.add( + BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_ALCOHOL_ID) ) - cls.banned_counter_customer.groups.add( - Group.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID) + cls.banned_counter_customer.ban_groups.add( + BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID) ) cls.beer = product_recipe.make( diff --git a/sith/settings.py b/sith/settings.py index a88734d3..42e46603 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -363,12 +363,13 @@ SITH_GROUP_OLD_SUBSCRIBERS_ID = 4 SITH_GROUP_ACCOUNTING_ADMIN_ID = 5 SITH_GROUP_COM_ADMIN_ID = 6 SITH_GROUP_COUNTER_ADMIN_ID = 7 -SITH_GROUP_BANNED_ALCOHOL_ID = 8 -SITH_GROUP_BANNED_COUNTER_ID = 9 -SITH_GROUP_BANNED_SUBSCRIPTION_ID = 10 -SITH_GROUP_SAS_ADMIN_ID = 11 -SITH_GROUP_FORUM_ADMIN_ID = 12 -SITH_GROUP_PEDAGOGY_ADMIN_ID = 13 +SITH_GROUP_SAS_ADMIN_ID = 8 +SITH_GROUP_FORUM_ADMIN_ID = 9 +SITH_GROUP_PEDAGOGY_ADMIN_ID = 10 + +SITH_GROUP_BANNED_ALCOHOL_ID = 11 +SITH_GROUP_BANNED_COUNTER_ID = 12 +SITH_GROUP_BANNED_SUBSCRIPTION_ID = 13 SITH_CLUB_REFOUND_ID = 89 SITH_COUNTER_REFOUND_ID = 38 From 4f35cc00bccc8f6cd808d086d6e720db91a64a48 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 3 Jan 2025 01:13:43 +0100 Subject: [PATCH 37/40] Add UserBan management views --- core/static/core/components/card.scss | 2 +- core/static/core/forms.scss | 8 -- core/templates/core/user_tools.jinja | 3 + core/views/forms.py | 27 ++++++- docs/reference/rootplace/forms.md | 7 ++ docs/reference/rootplace/views.md | 13 +++- mkdocs.yml | 1 + rootplace/forms.py | 49 ++++++++++++ rootplace/templates/rootplace/userban.jinja | 62 +++++++++++++++ rootplace/tests/__init__.py | 0 rootplace/tests/test_ban.py | 57 ++++++++++++++ .../{tests.py => tests/test_merge_users.py} | 0 rootplace/urls.py | 6 ++ rootplace/views.py | 76 ++++++++++--------- 14 files changed, 266 insertions(+), 45 deletions(-) create mode 100644 docs/reference/rootplace/forms.md create mode 100644 rootplace/forms.py create mode 100644 rootplace/templates/rootplace/userban.jinja create mode 100644 rootplace/tests/__init__.py create mode 100644 rootplace/tests/test_ban.py rename rootplace/{tests.py => tests/test_merge_users.py} (100%) diff --git a/core/static/core/components/card.scss b/core/static/core/components/card.scss index 1cbb2601..c8e59098 100644 --- a/core/static/core/components/card.scss +++ b/core/static/core/components/card.scss @@ -29,7 +29,7 @@ align-items: center; gap: 20px; - &:hover { + &.clickable:hover { background-color: darken($primary-neutral-light-color, 5%); } diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss index dd44aa8a..1d0fa1bc 100644 --- a/core/static/core/forms.scss +++ b/core/static/core/forms.scss @@ -199,14 +199,6 @@ form { } } - // ------------- LEGEND - - legend { - font-weight: var(--nf-label-font-weight); - display: block; - margin-bottom: calc(var(--nf-input-size) / 5); - } - .form-group, > p, > div { diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index d9a6c0c7..10f3fe9d 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -23,6 +23,9 @@
  • {% trans %}Operation logs{% endtrans %}
  • {% trans %}Delete user's forum messages{% endtrans %}
  • {% endif %} + {% if user.has_perm("core:view_userban") %} +
  • {% trans %}Bans{% endtrans %}
  • + {% endif %} {% if user.can_create_subscription or user.is_root %}
  • {% trans %}Subscriptions{% endtrans %}
  • {% endif %} diff --git a/core/views/forms.py b/core/views/forms.py index 5dbf8f3e..a0cdfb6b 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -21,6 +21,7 @@ # # import re +from datetime import date, datetime from io import BytesIO from captcha.fields import CaptchaField @@ -32,7 +33,14 @@ from django.contrib.staticfiles.management.commands.collectstatic import ( ) from django.core.exceptions import ValidationError from django.db import transaction -from django.forms import CheckboxSelectMultiple, DateInput, DateTimeInput, TextInput +from django.forms import ( + CheckboxSelectMultiple, + DateInput, + DateTimeInput, + TextInput, + Widget, +) +from django.utils.timezone import now from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from phonenumber_field.widgets import RegionalPhoneNumberWidget @@ -125,6 +133,23 @@ class SelectUser(TextInput): return output +# Fields + + +def validate_future_timestamp(value: date | datetime): + if value <= now(): + raise ValueError(_("Ensure this timestamp is set in the future")) + + +class FutureDateTimeField(forms.DateTimeField): + """A datetime field that accepts only future timestamps.""" + + default_validators = [validate_future_timestamp] + + def widget_attrs(self, widget: Widget) -> dict[str, str]: + return {"min": widget.format_value(now())} + + # Forms diff --git a/docs/reference/rootplace/forms.md b/docs/reference/rootplace/forms.md new file mode 100644 index 00000000..ca4fa328 --- /dev/null +++ b/docs/reference/rootplace/forms.md @@ -0,0 +1,7 @@ +::: rootplace.forms + handler: python + options: + members: + - MergeForm + - SelectUserForm + - BanForm \ No newline at end of file diff --git a/docs/reference/rootplace/views.md b/docs/reference/rootplace/views.md index 88a1b31b..87a26f6b 100644 --- a/docs/reference/rootplace/views.md +++ b/docs/reference/rootplace/views.md @@ -1 +1,12 @@ -::: rootplace.views \ No newline at end of file +::: rootplace.views + handler: python + options: + members: + - merge_users + - delete_all_forum_user_messages + - MergeUsersView + - DeleteAllForumUserMessagesView + - OperationLogListView + - BanView + - BanCreateView + - BanDeleteView \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 3777e2e4..70075794 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,6 +127,7 @@ nav: - reference/pedagogy/schemas.md - rootplace: - reference/rootplace/models.md + - reference/rootplace/forms.md - reference/rootplace/views.md - sas: - reference/sas/models.md diff --git a/rootplace/forms.py b/rootplace/forms.py new file mode 100644 index 00000000..5e7f8e94 --- /dev/null +++ b/rootplace/forms.py @@ -0,0 +1,49 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from core.models import User, UserBan +from core.views.forms import FutureDateTimeField, SelectDateTime +from core.views.widgets.select import AutoCompleteSelectUser + + +class MergeForm(forms.Form): + user1 = forms.ModelChoiceField( + label=_("User that will be kept"), + help_text=None, + required=True, + widget=AutoCompleteSelectUser, + queryset=User.objects.all(), + ) + user2 = forms.ModelChoiceField( + label=_("User that will be deleted"), + help_text=None, + required=True, + widget=AutoCompleteSelectUser, + queryset=User.objects.all(), + ) + + +class SelectUserForm(forms.Form): + user = forms.ModelChoiceField( + label=_("User to be selected"), + help_text=None, + required=True, + widget=AutoCompleteSelectUser, + queryset=User.objects.all(), + ) + + +class BanForm(forms.ModelForm): + """Form to ban a user.""" + + required_css_class = "required" + + class Meta: + model = UserBan + fields = ["user", "ban_group", "reason", "expires_at"] + field_classes = {"expires_at": FutureDateTimeField} + widgets = { + "user": AutoCompleteSelectUser, + "ban_group": forms.RadioSelect, + "expires_at": SelectDateTime, + } diff --git a/rootplace/templates/rootplace/userban.jinja b/rootplace/templates/rootplace/userban.jinja new file mode 100644 index 00000000..4510abf2 --- /dev/null +++ b/rootplace/templates/rootplace/userban.jinja @@ -0,0 +1,62 @@ +{% extends "core/base.jinja" %} + + +{% block additional_css %} + +{% endblock %} + + +{% block content %} + {% if user.has_perm("core:add_userban") %} + + + {% trans %}Ban a user{% endtrans %} + + {% endif %} + {% for user_ban in user_bans %} +
    + profil de {{ user_ban.user.get_short_name() }} +
    + + + {{ user_ban.user.get_full_name() }} + + + {{ user_ban.ban_group.name }} +

    {% trans %}Since{% endtrans %} : {{ user_ban.created_at|date }}

    +

    + {% trans %}Until{% endtrans %} : + {% if user_ban.expires_at %} + {{ user_ban.expires_at|date }} {{ user_ban.expires_at|time }} + {% else %} + {% trans %}not specified{% endtrans %} + {% endif %} +

    +
    + {% trans %}Reason{% endtrans %} +

    {{ user_ban.reason }}

    +
    + {% if user.has_perm("core:delete_userban") %} + + + {% trans %}Remove ban{% endtrans %} + + + {% endif %} +
    +
    + {% else %} +

    {% trans %}No active ban.{% endtrans %}

    + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/rootplace/tests/__init__.py b/rootplace/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rootplace/tests/test_ban.py b/rootplace/tests/test_ban.py new file mode 100644 index 00000000..4616630e --- /dev/null +++ b/rootplace/tests/test_ban.py @@ -0,0 +1,57 @@ +from datetime import datetime, timedelta + +import pytest +from django.contrib.auth.models import Permission +from django.test import Client +from django.urls import reverse +from django.utils.timezone import localtime +from model_bakery import baker +from pytest_django.asserts import assertRedirects + +from core.models import BanGroup, User, UserBan + + +@pytest.fixture +def operator(db) -> User: + return baker.make( + User, + user_permissions=Permission.objects.filter( + codename__in=["view_userban", "add_userban", "delete_userban"] + ), + ) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "expires_at", + [None, localtime().replace(second=0, microsecond=0) + timedelta(days=7)], +) +def test_ban_user(client: Client, operator: User, expires_at: datetime): + client.force_login(operator) + user = baker.make(User) + ban_group = BanGroup.objects.first() + data = { + "user": user.id, + "ban_group": ban_group.id, + "reason": "Being naughty", + } + if expires_at is not None: + data["expires_at"] = expires_at.strftime("%Y-%m-%d %H:%M") + response = client.post(reverse("rootplace:ban_create"), data) + assertRedirects(response, expected_url=reverse("rootplace:ban_list")) + bans = list(user.bans.all()) + assert len(bans) == 1 + assert bans[0].expires_at == expires_at + assert bans[0].reason == "Being naughty" + assert bans[0].ban_group == ban_group + + +@pytest.mark.django_db +def test_remove_ban(client: Client, operator: User): + client.force_login(operator) + user = baker.make(User) + ban = baker.make(UserBan, user=user) + assert user.bans.exists() + response = client.post(reverse("rootplace:ban_remove", kwargs={"ban_id": ban.id})) + assertRedirects(response, expected_url=reverse("rootplace:ban_list")) + assert not user.bans.exists() diff --git a/rootplace/tests.py b/rootplace/tests/test_merge_users.py similarity index 100% rename from rootplace/tests.py rename to rootplace/tests/test_merge_users.py diff --git a/rootplace/urls.py b/rootplace/urls.py index 81568558..6ba94c28 100644 --- a/rootplace/urls.py +++ b/rootplace/urls.py @@ -25,6 +25,9 @@ from django.urls import path from rootplace.views import ( + BanCreateView, + BanDeleteView, + BanView, DeleteAllForumUserMessagesView, MergeUsersView, OperationLogListView, @@ -38,4 +41,7 @@ urlpatterns = [ name="delete_forum_messages", ), path("logs/", OperationLogListView.as_view(), name="operation_logs"), + path("ban/", BanView.as_view(), name="ban_list"), + path("ban/new", BanCreateView.as_view(), name="ban_create"), + path("ban//remove/", BanDeleteView.as_view(), name="ban_remove"), ] diff --git a/rootplace/views.py b/rootplace/views.py index 818f0aa1..930f3b45 100644 --- a/rootplace/views.py +++ b/rootplace/views.py @@ -23,20 +23,19 @@ # import logging -from django import forms +from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.timezone import localdate -from django.utils.translation import gettext as _ -from django.views.generic import ListView -from django.views.generic.edit import FormView +from django.views.generic import DeleteView, ListView +from django.views.generic.edit import CreateView, FormView -from core.models import OperationLog, SithFile, User +from core.models import OperationLog, SithFile, User, UserBan from core.views import CanEditPropMixin -from core.views.widgets.select import AutoCompleteSelectUser from counter.models import Customer from forum.models import ForumMessageMeta +from rootplace.forms import BanForm, MergeForm, SelectUserForm def __merge_subscriptions(u1: User, u2: User): @@ -155,33 +154,6 @@ def delete_all_forum_user_messages( ForumMessageMeta(message=message, user=moderator, action="DELETE").save() -class MergeForm(forms.Form): - user1 = forms.ModelChoiceField( - label=_("User that will be kept"), - help_text=None, - required=True, - widget=AutoCompleteSelectUser, - queryset=User.objects.all(), - ) - user2 = forms.ModelChoiceField( - label=_("User that will be deleted"), - help_text=None, - required=True, - widget=AutoCompleteSelectUser, - queryset=User.objects.all(), - ) - - -class SelectUserForm(forms.Form): - user = forms.ModelChoiceField( - label=_("User to be selected"), - help_text=None, - required=True, - widget=AutoCompleteSelectUser, - queryset=User.objects.all(), - ) - - class MergeUsersView(FormView): template_name = "rootplace/merge.jinja" form_class = MergeForm @@ -233,3 +205,39 @@ class OperationLogListView(ListView, CanEditPropMixin): template_name = "rootplace/logs.jinja" ordering = ["-date"] paginate_by = 100 + + +class BanView(PermissionRequiredMixin, ListView): + """[UserBan][core.models.UserBan] management view. + + Displays : + + - the list of active bans with their main information, + with a link to [BanDeleteView][rootplace.views.BanDeleteView] for each one + - a link which redirects to [BanCreateView][rootplace.views.BanCreateView] + """ + + permission_required = "core.view_userban" + template_name = "rootplace/userban.jinja" + queryset = UserBan.objects.select_related("user", "user__profile_pict", "ban_group") + ordering = "created_at" + context_object_name = "user_bans" + + +class BanCreateView(PermissionRequiredMixin, CreateView): + """[UserBan][core.models.UserBan] creation view.""" + + permission_required = "core.add_userban" + form_class = BanForm + template_name = "core/create.jinja" + success_url = reverse_lazy("rootplace:ban_list") + + +class BanDeleteView(PermissionRequiredMixin, DeleteView): + """[UserBan][core.models.UserBan] deletion view.""" + + permission_required = "core.delete_userban" + pk_url_kwarg = "ban_id" + model = UserBan + template_name = "core/delete_confirm.jinja" + success_url = reverse_lazy("rootplace:ban_list") From e7215be00e7515fa9b09b47d053e323845a004f1 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 5 Jan 2025 14:19:03 +0100 Subject: [PATCH 38/40] translations --- locale/fr/LC_MESSAGES/django.po | 104 ++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 8540b562..f83f033e 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -921,8 +921,8 @@ msgstr "home" msgid "You can not make loops in clubs" msgstr "Vous ne pouvez pas faire de boucles dans les clubs" -#: club/models.py counter/models.py eboutic/models.py election/models.py -#: launderette/models.py sas/models.py trombi/models.py +#: club/models.py core/models.py counter/models.py eboutic/models.py +#: election/models.py launderette/models.py sas/models.py trombi/models.py msgid "user" msgstr "utilisateur" @@ -1009,6 +1009,7 @@ msgstr "Description" #: club/templates/club/club_members.jinja core/templates/core/user_clubs.jinja #: launderette/templates/launderette/launderette_admin.jinja +#: rootplace/templates/rootplace/userban.jinja msgid "Since" msgstr "Depuis" @@ -1787,7 +1788,7 @@ msgstr "Message d'alerte" msgid "Screens list" msgstr "Liste d'écrans" -#: com/views.py +#: com/views.py rootplace/templates/rootplace/userban.jinja msgid "Until" msgstr "Jusqu'à" @@ -1832,6 +1833,14 @@ msgstr "" msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" +#: core/models.py +msgid "ban group" +msgstr "groupe de ban" + +#: core/models.py +msgid "ban groups" +msgstr "groupes de ban" + #: core/models.py msgid "first name" msgstr "Prénom" @@ -1868,6 +1877,10 @@ msgstr "" "Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes " "les permissions de chacun de ses groupes." +#: core/models.py +msgid "The bans this user has received." +msgstr "Les bans que cet utilisateur a reçus." + #: core/models.py msgid "profile" msgstr "profil" @@ -2019,6 +2032,42 @@ msgstr "Profil" msgid "Visitor" msgstr "Visiteur" +#: core/models.py +msgid "ban type" +msgstr "type de ban" + +#: core/models.py +msgid "created at" +msgstr "créé le" + +#: core/models.py +msgid "expires at" +msgstr "expire le" + +#: core/models.py +msgid "" +"When the ban should be removed. Currently, there is no automatic removal, so " +"this is purely indicative. Automatic ban removal may be implemented later on." +msgstr "" +"Quand le ban devrait être retiré. Actuellement, il n'y a pas de retrait automatique, " +"donc ceci est purement indicatif. Le retrait automatique pourra être implémenté plus tard." + +#: core/models.py pedagogy/models.py +msgid "reason" +msgstr "raison" + +#: core/models.py +#, fuzzy +#| msgid "user" +msgid "user ban" +msgstr "utilisateur" + +#: core/models.py +#, fuzzy +#| msgid "user" +msgid "user bans" +msgstr "utilisateur" + #: core/models.py msgid "receive the Weekmail" msgstr "recevoir le Weekmail" @@ -3117,6 +3166,10 @@ msgstr "Journal d'opérations" msgid "Delete user's forum messages" msgstr "Supprimer les messages forum d'un utilisateur" +#: core/templates/core/user_tools.jinja +msgid "Bans" +msgstr "Bans" + #: core/templates/core/user_tools.jinja msgid "Subscriptions" msgstr "Cotisations" @@ -3260,6 +3313,10 @@ msgstr "Choisir un fichier" msgid "Choose user" msgstr "Choisir un utilisateur" +#: core/views/forms.py +msgid "Ensure this timestamp is set in the future" +msgstr "Assurez-vous que cet horodatage est dans le futur" + #: core/views/forms.py msgid "Username, email, or account number" msgstr "Nom d'utilisateur, email, ou numéro de compte AE" @@ -4882,10 +4939,6 @@ msgstr "signaler" msgid "reporter" msgstr "signalant" -#: pedagogy/models.py -msgid "reason" -msgstr "raison" - #: pedagogy/templates/pedagogy/guide.jinja #, python-format msgid "%(display_name)s" @@ -4917,7 +4970,8 @@ msgstr "non noté" msgid "UV comment moderation" msgstr "Modération des commentaires d'UV" -#: pedagogy/templates/pedagogy/moderation.jinja sas/models.py +#: pedagogy/templates/pedagogy/moderation.jinja +#: rootplace/templates/rootplace/userban.jinja sas/models.py msgid "Reason" msgstr "Raison" @@ -5038,6 +5092,18 @@ msgstr "Autocomplétion réussite" msgid "An error occurred: " msgstr "Une erreur est survenue : " +#: rootplace/forms.py +msgid "User that will be kept" +msgstr "Utilisateur qui sera conservé" + +#: rootplace/forms.py +msgid "User that will be deleted" +msgstr "Utilisateur qui sera supprimé" + +#: rootplace/forms.py +msgid "User to be selected" +msgstr "Utilisateur à sélectionner" + #: rootplace/templates/rootplace/delete_user_messages.jinja msgid "Delete all forum messages from an user" msgstr "Supprimer tous les messages forum d'un utilisateur" @@ -5067,17 +5133,21 @@ msgstr "Fusionner deux utilisateurs" msgid "Merge" msgstr "Fusion" -#: rootplace/views.py -msgid "User that will be kept" -msgstr "Utilisateur qui sera conservé" +#: rootplace/templates/rootplace/userban.jinja +msgid "Ban a user" +msgstr "Bannir un utilisateur" -#: rootplace/views.py -msgid "User that will be deleted" -msgstr "Utilisateur qui sera supprimé" +#: rootplace/templates/rootplace/userban.jinja +msgid "not specified" +msgstr "non spécifié" -#: rootplace/views.py -msgid "User to be selected" -msgstr "Utilisateur à sélectionner" +#: rootplace/templates/rootplace/userban.jinja +msgid "Remove ban" +msgstr "Retirer le ban" + +#: rootplace/templates/rootplace/userban.jinja +msgid "No active ban." +msgstr "Pas de ban actif" #: sas/forms.py msgid "Add a new album" From a8702d4f5eccb1900eb2b5003c3bbc066ffc2267 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 5 Jan 2025 16:42:26 +0100 Subject: [PATCH 39/40] Improve welcome page * Improve code readability of calendar details * Add link to AE Dev discord in useful links * Add link to github at the bottom --- .../com/components/ics-calendar-index.ts | 19 ++++++++----------- com/templates/com/news_list.jinja | 8 ++++---- core/static/core/colors.scss | 1 + core/static/core/style.scss | 4 ++++ core/templates/core/base.jinja | 3 ++- locale/fr/LC_MESSAGES/django.po | 19 ++++++++++++------- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index e3baddc6..11a15a32 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -77,17 +77,14 @@ export class IcsCalendar extends inheritHtmlElement("div") { const makePopupTitle = (event: EventImpl) => { const row = document.createElement("div"); - const title = document.createElement("h4"); - const time = document.createElement("span"); - - title.setAttribute("class", "event-details-row-content"); - title.textContent = event.title; - - time.setAttribute("class", "event-details-row-content"); - time.textContent = `${this.formatDate(event.start)} - ${this.formatDate(event.end)}`; - - row.appendChild(title); - row.appendChild(time); + row.innerHTML = ` +

    + ${event.title} +

    + + ${this.formatDate(event.start)} - ${this.formatDate(event.end)} + + `; return makePopupInfo( row, "fa-solid fa-calendar-days fa-xl event-detail-row-icon", diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 8f20ce19..34371298 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -123,7 +123,10 @@
    {% endblock %} - - - diff --git a/core/static/core/colors.scss b/core/static/core/colors.scss index e10eb905..9ed493ba 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -26,6 +26,7 @@ $faceblue: hsl(221, 44%, 41%); $twitblue: hsl(206, 82%, 63%); $discordblurple: #7289da; $instagradient: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%); +$githubblack: rgb(22, 22, 20); $shadow-color: rgb(223, 223, 223); diff --git a/core/static/core/style.scss b/core/static/core/style.scss index 2f3af9f7..913733d6 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -822,6 +822,10 @@ footer { margin-top: 3px; color: rgba(0, 0, 0, 0.3); } + + .fa-github { + color: $githubblack; + } } diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index d089ec9f..17b9befa 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -108,7 +108,8 @@ {% trans %}Help & Documentation{% endtrans %} {% trans %}R&D{% endtrans %}
    - + + {% trans %}Site created by the IT Department of the AE{% endtrans %} {% endblock %} diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index f83f033e..fc24a41d 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-05 15:28+0100\n" +"POT-Creation-Date: 2025-01-05 16:39+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -1588,8 +1588,12 @@ msgid "Social media" msgstr "Réseaux sociaux" #: com/templates/com/news_list.jinja -msgid "Discord" -msgstr "Discord" +msgid "Discord AE" +msgstr "Discord AE" + +#: com/templates/com/news_list.jinja +msgid "Dev Team" +msgstr "Pôle Informatique" #: com/templates/com/news_list.jinja msgid "Facebook" @@ -2049,8 +2053,9 @@ msgid "" "When the ban should be removed. Currently, there is no automatic removal, so " "this is purely indicative. Automatic ban removal may be implemented later on." msgstr "" -"Quand le ban devrait être retiré. Actuellement, il n'y a pas de retrait automatique, " -"donc ceci est purement indicatif. Le retrait automatique pourra être implémenté plus tard." +"Quand le ban devrait être retiré. Actuellement, il n'y a pas de retrait " +"automatique, donc ceci est purement indicatif. Le retrait automatique pourra " +"être implémenté plus tard." #: core/models.py pedagogy/models.py msgid "reason" @@ -3426,8 +3431,8 @@ msgid "" "Describe the product. If it's an event's click, give some insights about it, " "like the date (including the year)." msgstr "" -"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques détails " -"dessus, comme la date (en incluant l'année)." +"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques " +"détails dessus, comme la date (en incluant l'année)." #: counter/management/commands/dump_accounts.py msgid "Your AE account has been emptied" From 25298518bc6b3d1a1a0c694a035cfe27bdc38bf9 Mon Sep 17 00:00:00 2001 From: Sli Date: Sun, 5 Jan 2025 17:25:23 +0100 Subject: [PATCH 40/40] fix: wrong link for ae dev discord --- com/templates/com/news_list.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com/templates/com/news_list.jinja b/com/templates/com/news_list.jinja index 34371298..e0f5c4e5 100644 --- a/com/templates/com/news_list.jinja +++ b/com/templates/com/news_list.jinja @@ -125,7 +125,7 @@ {% trans %}Discord AE{% endtrans %} {% if user.was_subscribed %} - - {% trans %}Dev Team{% endtrans %} + - {% trans %}Dev Team{% endtrans %} {% endif %}