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/club/admin.py b/club/admin.py index c2444c17..04265245 100644 --- a/club/admin.py +++ b/club/admin.py @@ -20,6 +20,14 @@ 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", + "home", + "page", + ) @admin.register(Membership) 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..f3d3a1e9 --- /dev/null +++ b/club/migrations/0012_club_board_group_club_members_group.py @@ -0,0 +1,106 @@ +# 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 +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") + + 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, + defaults={"is_meta": True}, + )[0] + club.members_group = meta_groups.get_or_create( + name=club.unix_name + settings.SITH_MEMBER_SUFFIX, + defaults={"is_meta": True}, + )[0] + 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 : +# - 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.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.PROTECT, + 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.PROTECT, + related_name="club", + to="core.group", + ), + ), + migrations.RunPython( + migrate_meta_groups, reverse_code=migrations.RunPython.noop, elidable=True + ), + ] 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/club/models.py b/club/models.py index 5300057d..4184715a 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,14 +31,14 @@ 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 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. @@ -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", @@ -103,6 +90,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.PROTECT + ) + board_group = models.OneToOneField( + Group, related_name="club_board", on_delete=models.PROTECT + ) class Meta: ordering = ["name", "unix_name"] @@ -112,23 +105,27 @@ 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: + 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 + ) + 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.make_page() cache.set(f"sith_club_{self.unix_name}", self) @@ -136,7 +133,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 +152,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 +193,34 @@ 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) + self.board_group.delete() + self.members_group.delete() + 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 + return user.is_root or 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,9 +241,8 @@ class Club(models.Model): cache.set(f"membership_{self.id}_{user.id}", membership) return membership - def has_rights_in_club(self, user): - m = self.get_membership_for(user) - return m is not None and m.role > settings.SITH_MAXIMUM_FREE_ROLE + def has_rights_in_club(self, user: User) -> bool: + return user.is_in_group(pk=self.board_group_id) class MembershipQuerySet(models.QuerySet): @@ -283,42 +261,65 @@ class MembershipQuerySet(models.QuerySet): """ return self.filter(role__gt=settings.SITH_MAXIMUM_FREE_ROLE) - def update(self, **kwargs): - """Refresh the cache for the elements of the queryset. + def update(self, **kwargs) -> int: + """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: - # 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 = {} + 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] = "not_member" + cache.set_many(cache_memberships) + 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 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")) - nb_rows, _ = super().delete() + memberships = set(self.all()) + 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") + Membership._remove_club_groups(memberships) + cache.set_many( + { + f"membership_{m.club_id}_{m.user_id}": "not_member" + for m in memberships + } + ) + return nb_rows, rows_counts class Membership(models.Model): @@ -361,6 +362,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} " @@ -370,7 +378,14 @@ class Membership(models.Model): def save(self, *args, **kwargs): super().save(*args, **kwargs) + # 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") @@ -378,11 +393,11 @@ class Membership(models.Model): def get_absolute_url(self): return reverse("club:club_members", kwargs={"club_id": self.club_id}) - 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.""" @@ -392,9 +407,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 ae055bd0..b81aa38d 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 @@ -164,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) @@ -182,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): @@ -192,10 +227,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 +507,35 @@ 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_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) + 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_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) - 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) + 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.""" @@ -517,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.""" 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/com/api.py b/com/api.py new file mode 100644 index 00000000..e46daea9 --- /dev/null +++ b/com/api.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from django.conf import settings +from django.http import Http404 +from ninja_extra import ControllerBase, api_controller, route + +from com.calendar import IcsCalendar +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", url_name="calendar_external") + 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 + 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. + + 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", url_name="calendar_internal") + def calendar_internal(self): + 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/calendar.py b/com/calendar.py new file mode 100644 index 00000000..9003d6de --- /dev/null +++ b/com/calendar.py @@ -0,0 +1,76 @@ +from datetime import datetime, timedelta +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 ical.calendar import Calendar +from ical.calendar_stream import IcsCalendarStream +from ical.event import 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() - (relativedelta(months=6)), + ).prefetch_related("news"): + event = Event( + 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.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(IcsCalendarStream.calendar_to_ics(calendar).encode("utf-8")) + return cls._INTERNAL_CALENDAR diff --git a/com/models.py b/com/models.py index f3076174..633c7671 100644 --- a/com/models.py +++ b/com/models.py @@ -17,11 +17,12 @@ # 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 django.conf import settings from django.core.exceptions import ValidationError from django.core.mail import EmailMultiAlternatives diff --git a/com/signals.py b/com/signals.py new file mode 100644 index 00000000..ea004ad8 --- /dev/null +++ b/com/signals.py @@ -0,0 +1,10 @@ +from django.db.models.base import post_save +from django.dispatch import receiver + +from com.calendar import IcsCalendar +from com.models import News + + +@receiver(post_save, sender=News, dispatch_uid="update_internal_ics") +def update_internal_ics(*args, **kwargs): + _ = IcsCalendar.make_internal() 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..11a15a32 --- /dev/null +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -0,0 +1,194 @@ +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"; +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", + }; + } + + 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(); + } + + const makePopupInfo = (info: HTMLElement, iconClass: string) => { + const row = document.createElement("div"); + const icon = document.createElement("i"); + + row.setAttribute("class", "event-details-row"); + + 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"); + 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", + ); + }; + + const makePopupLocation = (event: EventImpl) => { + if (event.extendedProps.location === null) { + return null; + } + const info = document.createElement("div"); + info.innerText = event.extendedProps.location; + + 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 + const popup = document.createElement("div"); + const popupContainer = document.createElement("div"); + + popup.setAttribute("id", "event-details"); + popupContainer.setAttribute("class", "event-details-container"); + + 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 + // 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, { + 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()); + }, + 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); + }, + }); + 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 new file mode 100644 index 00000000..21aa55d7 --- /dev/null +++ b/com/static/com/components/ics-calendar.scss @@ -0,0 +1,101 @@ +@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; + --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); + gap: 20px; + } + + .event-detail-row-icon { + margin-left: 10px; + margin-right: 20px; + align-content: center; + align-self: center; + } + + .event-details-row { + display: flex; + align-items: start; + } + + .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; + } + } + + 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-x: 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 diff --git a/com/static/com/css/news-detail.scss b/com/static/com/css/news-detail.scss new file mode 100644 index 00000000..c0d633fb --- /dev/null +++ b/com/static/com/css/news-detail.scss @@ -0,0 +1,61 @@ +@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; + } +} diff --git a/com/static/com/css/news-list.scss b/com/static/com/css/news-list.scss new file mode 100644 index 00000000..bcbf8273 --- /dev/null +++ b/com/static/com/css/news-list.scss @@ -0,0 +1,297 @@ +@import "core/static/core/colors"; +@import "core/static/core/devices"; + +#news { + display: flex; + + @media (max-width: 800px) { + flex-direction: column; + } + + #news_admin { + margin-bottom: 1em; + } + + #right_column { + flex: 20%; + 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: 17px; + + &:not(:first-of-type) { + margin: 2em 0 1em 0; + } + } + + @media screen and (max-width: $small-devices) { + + #left_column, + #right_column { + flex: 100%; + } + } + + /* LINKS/BIRTHDAYS */ + #links, + #birthdays { + display: block; + width: 100%; + background: white; + font-size: 70%; + margin-bottom: 1em; + + h3 { + margin-bottom: 0; + } + + #links_content { + overflow: auto; + box-shadow: $shadow-color 1px 1px 1px; + height: 20em; + + h4 { + margin-left: 5px; + } + + 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; + } + } + } + + } + + #birthdays_content { + 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..e0f5c4e5 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 %}
@@ -83,60 +92,58 @@
{% 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 %}

- + + + -
-
-
{% 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 -%}
  • @@ -150,14 +157,13 @@ type="EVENT").order_by('dates__start_date') %} {%- 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 -%}
+
+
{% 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/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/tests/test_api.py b/com/tests/test_api.py new file mode 100644 index 00000000..f131052e --- /dev/null +++ b/com/tests/test_api.py @@ -0,0 +1,122 @@ +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 + + +@dataclass +class MockResponse: + status: int + value: str + + @property + def data(self): + 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): + 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): + 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 + 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 + + +@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() diff --git a/com/tests.py b/com/tests/test_views.py similarity index 93% rename from com/tests.py rename to com/tests/test_views.py index 399eb0e8..3f98bfdc 100644 --- a/com/tests.py +++ b/com/tests/test_views.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/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): diff --git a/core/admin.py b/core/admin.py index 367c056a..5de89ada 100644 --- a/core/admin.py +++ b/core/admin.py @@ -15,17 +15,32 @@ 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) @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",) + 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) @@ -37,10 +52,24 @@ class UserAdmin(admin.ModelAdmin): "profile_pict", "avatar_pict", "scrub_pict", + "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",) + + @admin.register(Page) class PageAdmin(admin.ModelAdmin): list_display = ("name", "_full_name", "owner_group") 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..e3d6d8e4 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -46,8 +46,9 @@ from accounting.models import ( SimplifiedAccountingType, ) 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, RealGroup, 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 @@ -93,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, @@ -143,7 +145,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 +370,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 +594,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 +603,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) @@ -741,7 +740,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 @@ -767,8 +766,9 @@ Welcome to the wiki page! ] ) NewsDate.objects.bulk_create(news_dates) + IcsCalendar.make_internal() # Force refresh of the calendar after a bulk_create - # Create som data for pedagogy + # Create some data for pedagogy UV( code="PA00", @@ -889,7 +889,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 +911,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 +933,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 +952,15 @@ 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") + 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 +968,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( @@ -984,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/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") 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..8297ebf7 --- /dev/null +++ b/core/migrations/0041_delete_metagroup_alter_group_options_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.16 on 2024-11-30 13:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + 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( + 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", + ), + ), + ] 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 + ), + ] 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 c8375727..278182ac 100644 --- a/core/models.py +++ b/core/models.py @@ -36,14 +36,13 @@ 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 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 @@ -58,33 +57,15 @@ 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"] + description = models.TextField(_("description")) def get_absolute_url(self) -> str: return reverse("core:group_list") @@ -100,65 +81,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,13 +126,35 @@ 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 +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 @@ -261,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", @@ -438,18 +388,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 +412,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 @@ -514,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: @@ -821,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 @@ -943,7 +926,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 +941,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/static/core/colors.scss b/core/static/core/colors.scss index 35dc6a69..9ed493ba 100644 --- a/core/static/core/colors.scss +++ b/core/static/core/colors.scss @@ -24,6 +24,9 @@ $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%); +$githubblack: rgb(22, 22, 20); $shadow-color: rgb(223, 223, 223); 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/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/forms.scss b/core/static/core/forms.scss index ae6f8a21..1d0fa1bc 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 { @@ -198,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/static/core/style.scss b/core/static/core/style.scss index a9205e23..913733d6 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 { @@ -1420,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/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/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/tests/test_core.py b/core/tests/test_core.py index a33a8705..878db4e4 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,56 +350,35 @@ 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") 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 = 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) for group in ( self.root_group, - self.banned_counters, self.accounting_admin, 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 +387,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/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): diff --git a/core/views/forms.py b/core/views/forms.py index bda33ec0..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 @@ -37,14 +38,16 @@ from django.forms import ( 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 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, @@ -130,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 @@ -293,7 +313,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/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): 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/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/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/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/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/docs/tutorial/groups.md b/docs/tutorial/groups.md index b8603d16..fbb58aab 100644 --- a/docs/tutorial/groups.md +++ b/docs/tutorial/groups.md @@ -1,54 +1,165 @@ -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") + ``` + +!!!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. -- 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 +173,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/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) 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 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..d0dea4a5 100644 --- a/galaxy/management/commands/generate_galaxy_test_data.py +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -118,18 +118,26 @@ 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 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()) + # 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}", + 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) + ] + ) def make_users(self): """Create all the users and store them in `self.users` for fast access later. diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index d6c2e813..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: 2024-12-23 02:37+0100\n" +"POT-Creation-Date: 2025-01-05 16:39+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:356 +#: 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:345 -#: 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:351 counter/models.py:997 -#: 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:352 counter/models.py:998 -#: 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:714 +#: 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: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 -#: 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:1034 -#: pedagogy/models.py:208 +#: accounting/models.py counter/models.py pedagogy/models.py msgid "comment" msgstr "commentaire" -#: accounting/models.py:259 counter/models.py:726 counter/models.py:831 -#: 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:1487 core/models.py:1513 -#: counter/models.py:795 +#: 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:504 -#: 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:408 -#: 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:303 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,343 @@ 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:79 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:125 -#: 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/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:46 +#: 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 +578,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 +587,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 +651,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:711 +#: club/forms.py club/tests.py msgid "This field is required" msgstr "Ce champ est obligatoire" -#: club/forms.py:118 club/tests.py:742 +#: 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:698 club/tests.py:724 +#: 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:207 +#: 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:210 -#: 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,298 +893,274 @@ 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:358 +#: club/models.py core/models.py msgid "address" msgstr "Adresse" -#: club/models.py:98 core/models.py:269 +#: club/models.py core/models.py msgid "home" msgstr "home" -#: club/models.py:150 +#: 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: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 -#: 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 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" -#: club/models.py:354 core/models.py:322 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:359 core/models.py:84 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:415 club/models.py:510 +#: club/models.py msgid "Email address" msgstr "Adresse email" -#: club/models.py:423 +#: 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:427 com/models.py:97 com/models.py:322 core/models.py:906 +#: club/models.py com/models.py core/models.py msgid "is moderated" msgstr "est modéré" -#: club/models.py:431 com/models.py:101 com/models.py:326 +#: club/models.py com/models.py msgid "moderator" msgstr "modérateur" -#: club/models.py:457 +#: club/models.py 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 club/templates/club/mailing.jinja msgid "Mailing" msgstr "Liste de diffusion" -#: club/models.py:520 +#: 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:528 club/tests.py:770 +#: 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:556 +#: 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 +#: rootplace/templates/rootplace/userban.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:309 -#: 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:43 -#: 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/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" @@ -1309,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:89 +#: club/views.py msgid "Members" msgstr "Membres" -#: club/views.py:98 +#: club/views.py msgid "Old members" msgstr "Anciens membres" -#: club/views.py:108 core/templates/core/page.jinja:33 +#: club/views.py core/templates/core/page.jinja msgid "History" msgstr "Historique" -#: club/views.py:116 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:136 +#: club/views.py msgid "Edit club page" msgstr "Éditer la page de club" -#: club/views.py:145 club/views.py:452 +#: club/views.py msgid "Sellings" msgstr "Vente" -#: club/views.py:152 +#: club/views.py msgid "Mailing list" msgstr "Listes de diffusion" -#: club/views.py:161 com/views.py:134 +#: club/views.py com/views.py msgid "Posters list" msgstr "Liste d'affiches" -#: club/views.py:171 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 ?)" @@ -1446,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:1457 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:871 core/models.py:921 +#: 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)" @@ -1723,183 +1535,203 @@ 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 -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 -msgid "Agenda" -msgstr "Agenda" +#: com/templates/com/news_list.jinja +msgid "Links" +msgstr "Liens" -#: com/templates/com/news_list.jinja:137 +#: 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 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" +msgstr "Facebook" + +#: com/templates/com/news_list.jinja +msgid "Instagram" +msgstr "Instagram" + +#: 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 -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/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" -#: 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 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 +#: 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 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 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!" @@ -1907,133 +1739,141 @@ 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 rootplace/templates/rootplace/userban.jinja 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:79 -msgid "meta group status" -msgstr "status du meta-groupe" +#: core/models.py +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 +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 #, 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 +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" -#: core/models.py:251 +#: core/models.py msgid "last name" msgstr "Nom" -#: core/models.py:252 +#: core/models.py msgid "email address" msgstr "adresse email" -#: core/models.py:253 +#: core/models.py msgid "date of birth" msgstr "date de naissance" -#: core/models.py:254 +#: core/models.py msgid "nick name" msgstr "surnom" -#: core/models.py:255 +#: core/models.py msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:258 +#: core/models.py msgid "groups" msgstr "groupes" -#: core/models.py:260 +#: core/models.py msgid "" "The groups this user belongs to. A user will get all permissions granted to " "each of their groups." @@ -2041,261 +1881,289 @@ 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 +msgid "The bans this user has received." +msgstr "Les bans que cet utilisateur a reçus." + +#: core/models.py msgid "profile" msgstr "profil" -#: core/models.py:285 +#: core/models.py msgid "avatar" msgstr "avatar" -#: core/models.py:293 +#: core/models.py msgid "scrub" msgstr "blouse" -#: core/models.py:299 +#: core/models.py msgid "sex" msgstr "Genre" -#: core/models.py:303 +#: core/models.py msgid "Man" msgstr "Homme" -#: core/models.py:303 +#: core/models.py msgid "Woman" msgstr "Femme" -#: core/models.py:305 +#: core/models.py msgid "pronouns" msgstr "pronoms" -#: core/models.py:307 +#: core/models.py msgid "tshirt size" msgstr "taille de t-shirt" -#: core/models.py:310 +#: core/models.py msgid "-" msgstr "-" -#: core/models.py:311 +#: core/models.py msgid "XS" msgstr "XS" -#: core/models.py:312 +#: core/models.py msgid "S" msgstr "S" -#: core/models.py:313 +#: core/models.py msgid "M" msgstr "M" -#: core/models.py:314 +#: core/models.py msgid "L" msgstr "L" -#: core/models.py:315 +#: core/models.py msgid "XL" msgstr "XL" -#: core/models.py:316 +#: core/models.py msgid "XXL" msgstr "XXL" -#: core/models.py:317 +#: core/models.py msgid "XXXL" msgstr "XXXL" -#: core/models.py:325 +#: core/models.py msgid "Student" msgstr "Étudiant" -#: core/models.py:326 +#: core/models.py msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:327 +#: core/models.py msgid "Teacher" msgstr "Enseignant" -#: core/models.py:328 +#: core/models.py msgid "Agent" msgstr "Personnel" -#: core/models.py:329 +#: core/models.py msgid "Doctor" msgstr "Doctorant" -#: core/models.py:330 +#: core/models.py msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:331 +#: core/models.py msgid "Service" msgstr "Service" -#: core/models.py:337 +#: core/models.py msgid "department" msgstr "département" -#: core/models.py:344 +#: core/models.py msgid "dpt option" msgstr "Filière" -#: core/models.py:346 pedagogy/models.py:70 pedagogy/models.py:294 +#: core/models.py pedagogy/models.py msgid "semester" msgstr "semestre" -#: core/models.py:347 +#: core/models.py msgid "quote" msgstr "citation" -#: core/models.py:348 +#: core/models.py msgid "school" msgstr "école" -#: core/models.py:350 +#: core/models.py msgid "promo" msgstr "promo" -#: core/models.py:353 +#: core/models.py msgid "forum signature" msgstr "signature du forum" -#: core/models.py:355 +#: core/models.py msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:357 +#: core/models.py msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:360 +#: core/models.py msgid "parent address" msgstr "adresse des parents" -#: core/models.py:363 +#: core/models.py msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:556 +#: 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:710 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:821 +#: core/models.py msgid "Visitor" msgstr "Visiteur" -#: core/models.py:828 +#: 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" -#: core/models.py:829 +#: core/models.py msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:831 +#: core/models.py msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:834 +#: core/models.py 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 sas/forms.py msgid "file name" msgstr "nom du fichier" -#: core/models.py:864 core/models.py:1215 +#: core/models.py msgid "parent" msgstr "parent" -#: core/models.py:878 +#: core/models.py msgid "compressed file" msgstr "version allégée" -#: core/models.py:885 +#: core/models.py msgid "thumbnail" msgstr "miniature" -#: core/models.py:893 core/models.py:910 +#: core/models.py msgid "owner" msgstr "propriétaire" -#: core/models.py:897 core/models.py:1232 +#: core/models.py msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:900 core/models.py:1235 +#: core/models.py msgid "view group" msgstr "groupe de vue" -#: core/models.py:902 +#: core/models.py msgid "is folder" msgstr "est un dossier" -#: core/models.py:903 +#: core/models.py msgid "mime type" msgstr "type mime" -#: core/models.py:904 +#: core/models.py msgid "size" msgstr "taille" -#: core/models.py:915 +#: core/models.py msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:917 +#: core/models.py msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:984 +#: 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:986 core/models.py:990 +#: core/models.py msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:993 +#: 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:1004 +#: core/models.py msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:1021 +#: core/models.py msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1198 +#: core/models.py msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1204 +#: core/models.py msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2303,403 +2171,372 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1222 +#: core/models.py msgid "page name" msgstr "nom de la page" -#: core/models.py:1227 +#: core/models.py msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1240 +#: core/models.py msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1247 +#: core/models.py msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1297 +#: core/models.py msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1300 +#: core/models.py msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1411 +#: core/models.py msgid "revision" msgstr "révision" -#: core/models.py:1412 +#: core/models.py msgid "page title" msgstr "titre de la page" -#: core/models.py:1413 +#: core/models.py msgid "page content" msgstr "contenu de la page" -#: core/models.py:1454 +#: core/models.py msgid "url" msgstr "url" -#: core/models.py:1455 +#: core/models.py msgid "param" msgstr "param" -#: core/models.py:1460 +#: core/models.py msgid "viewed" msgstr "vue" -#: core/models.py:1518 +#: 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 -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 -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:125 -#: 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:170 -#: 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." @@ -2707,72 +2544,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." @@ -2780,19 +2617,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." @@ -2800,93 +2637,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: " @@ -2894,15 +2729,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" @@ -2912,7 +2747,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" @@ -2922,7 +2757,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 " @@ -2931,27 +2766,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 " @@ -2969,318 +2804,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:69 +#: 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:98 +#: core/templates/core/user_edit.jinja msgid "Edit user profile" msgstr "Éditer le profil de l'utilisateur" -#: core/templates/core/user_edit.jinja:160 +#: core/templates/core/user_edit.jinja msgid "Change my password" msgstr "Changer mon mot de passe" -#: core/templates/core/user_edit.jinja:165 +#: core/templates/core/user_edit.jinja msgid "Change user password" msgstr "Changer le mot de passe" -#: core/templates/core/user_edit.jinja:175 +#: core/templates/core/user_edit.jinja msgid "Username:" msgstr "Nom d'utilisateur : " -#: core/templates/core/user_edit.jinja:178 +#: 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:149 +#: 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" @@ -3292,214 +3118,215 @@ 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:295 -#: 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 "Bans" +msgstr "Bans" + +#: 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:180 -#: 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:270 core/views/forms.py:277 -#: 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:95 core/views/forms.py:103 +#: core/views/forms.py msgid "Choose file" msgstr "Choisir un fichier" -#: core/views/forms.py:119 core/views/forms.py:127 +#: core/views/forms.py msgid "Choose user" msgstr "Choisir un utilisateur" -#: core/views/forms.py:159 +#: 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" -#: core/views/forms.py:220 +#: core/views/forms.py msgid "" "Profile: you need to be visible on the picture, in order to be recognized (e." "g. by the barmen)" @@ -3507,286 +3334,292 @@ 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 msgid "Avatar: used on the forum" msgstr "Avatar : utilisé sur le forum" -#: core/views/forms.py:229 +#: 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:281 +#: 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:306 +#: core/views/forms.py msgid "Godfather / Godmother" msgstr "Parrain / Marraine" -#: core/views/forms.py:307 +#: core/views/forms.py msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:312 counter/forms.py:78 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:326 +#: core/views/forms.py msgid "This user does not exist" msgstr "Cet utilisateur n'existe pas" -#: core/views/forms.py:328 +#: 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:340 +#: core/views/forms.py #, 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 #, 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:833 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:994 -#: counter/models.py:1030 launderette/models.py:32 +#: counter/apps.py counter/models.py launderette/models.py msgid "counter" msgstr "comptoir" -#: counter/forms.py:59 +#: counter/forms.py msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:108 +#: counter/forms.py msgid "User not found" msgstr "Utilisateur non trouvé" -#: counter/management/commands/dump_accounts.py:148 +#: 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é" -#: 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:1128 +#: 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:732 +#: counter/models.py msgid "bank" msgstr "banque" -#: counter/models.py:734 counter/models.py:836 +#: counter/models.py msgid "is validated" msgstr "est validé" -#: counter/models.py:739 +#: counter/models.py msgid "refilling" msgstr "rechargement" -#: counter/models.py:813 eboutic/models.py:249 +#: counter/models.py eboutic/models.py msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:814 counter/models.py:1108 eboutic/models.py:250 +#: counter/models.py eboutic/models.py msgid "quantity" msgstr "quantité" -#: counter/models.py:833 +#: counter/models.py msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:841 +#: counter/models.py msgid "selling" msgstr "vente" -#: counter/models.py:945 +#: counter/models.py msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:946 +#: 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:948 counter/models.py:961 +#: counter/models.py #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3798,177 +3631,174 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:999 +#: counter/models.py msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:1002 +#: counter/models.py msgid "permanency" msgstr "permanence" -#: counter/models.py:1035 +#: counter/models.py msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:1038 +#: counter/models.py msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1104 +#: counter/models.py msgid "cash summary" msgstr "relevé" -#: counter/models.py:1107 +#: counter/models.py msgid "value" msgstr "valeur" -#: counter/models.py:1110 +#: counter/models.py msgid "check" msgstr "chèque" -#: counter/models.py:1112 +#: 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:1116 +#: counter/models.py msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1132 +#: counter/models.py msgid "banner" msgstr "bannière" -#: counter/models.py:1134 +#: counter/models.py msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1136 +#: counter/models.py msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1138 +#: counter/models.py msgid "secret" msgstr "secret" -#: counter/models.py:1177 +#: counter/models.py msgid "uid" msgstr "uid" -#: counter/models.py:1182 counter/models.py:1187 +#: counter/models.py msgid "student card" msgstr "carte étudiante" -#: counter/models.py:1188 +#: 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:55 -#: 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:66 +#: counter/templates/counter/counter_click.jinja msgid "Select a product..." msgstr "Sélectionnez un produit…" -#: counter/templates/counter/counter_click.jinja:68 +#: counter/templates/counter/counter_click.jinja msgid "Operations" msgstr "Opérations" -#: counter/templates/counter/counter_click.jinja:69 +#: counter/templates/counter/counter_click.jinja msgid "Confirm (FIN)" msgstr "Confirmer (FIN)" -#: counter/templates/counter/counter_click.jinja:70 +#: counter/templates/counter/counter_click.jinja msgid "Cancel (ANN)" msgstr "Annuler (ANN)" -#: counter/templates/counter/counter_click.jinja:81 -#: 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:89 -#: 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:95 +#: counter/templates/counter/counter_click.jinja msgid "This basket is empty" msgstr "Votre panier est vide" -#: counter/templates/counter/counter_click.jinja:124 +#: counter/templates/counter/counter_click.jinja msgid "Finish" msgstr "Terminer" -#: counter/templates/counter/counter_click.jinja:130 -#: 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:140 +#: 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 " @@ -3978,127 +3808,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:161 +#: 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 " @@ -4107,13 +3932,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." @@ -4121,12 +3946,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' " @@ -4135,7 +3960,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 " @@ -4148,7 +3973,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." @@ -4156,7 +3981,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." @@ -4164,7 +3989,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 " @@ -4176,54 +4001,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." @@ -4231,206 +4054,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:95 +#: 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 " @@ -4441,27 +4258,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:103 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "" "You must fill your billing infos if you want to pay with your credit\n" " card" @@ -4469,7 +4286,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 msgid "" "\n" " The Crédit Agricole changed its policy related to the " @@ -4487,564 +4304,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:124 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Pay with credit card" msgstr "Payer avec une carte bancaire" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:129 +#: 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:131 +#: 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:136 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Pay with Sith account" msgstr "Payer avec un compte AE" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:147 +#: eboutic/templates/eboutic/eboutic_makecommand.jinja msgid "Billing info registration success" msgstr "Informations de facturation enregistrées" -#: eboutic/templates/eboutic/eboutic_makecommand.jinja:148 +#: 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" @@ -5052,223 +4848,211 @@ 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 -msgid "reason" -msgstr "raison" - -#: pedagogy/templates/pedagogy/guide.jinja:5 -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 +#: rootplace/templates/rootplace/userban.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." @@ -5276,53 +5060,64 @@ 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/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" -#: 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." @@ -5331,114 +5126,117 @@ 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 -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:167 -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:177 -msgid "User to be selected" -msgstr "Utilisateur à sélectionner" +#: rootplace/templates/rootplace/userban.jinja +msgid "Remove ban" +msgstr "Retirer le ban" -#: sas/forms.py:16 +#: rootplace/templates/rootplace/userban.jinja +msgid "No active ban." +msgstr "Pas de ban actif" + +#: 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." @@ -5447,437 +5245,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 " @@ -5886,31 +5684,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." @@ -5919,46 +5717,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)" @@ -5966,11 +5764,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)" @@ -5978,74 +5776,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)" @@ -6053,67 +5846,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)" @@ -6121,31 +5913,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 " @@ -6162,16 +5954,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 " @@ -6181,19 +5973,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." @@ -6201,23 +5993,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" - -#~ 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" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 7412eac5..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: 2024-12-23 02:38+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,168 +17,172 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: core/static/bundled/core/components/ajax-select-base.ts:68 +#: 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" -#: 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" 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/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..695b4503 100644 --- a/poetry.lock +++ b/poetry.lock @@ -931,6 +931,23 @@ files = [ {file = "hiredis-3.1.0.tar.gz", hash = "sha256:51d40ac3611091020d7dea6b05ed62cb152bff595fa4f931e7b6479d777acf7c"}, ] +[[package]] +name = "ical" +version = "8.3.0" +description = "Python iCalendar implementation (rfc 2445)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ical-8.3.0-py3-none-any.whl", hash = "sha256:606f2f561bd8b75cb726710dddbb20f3f84dfa1d6323550947dba97359423850"}, + {file = "ical-8.3.0.tar.gz", hash = "sha256:e277cc518cbb0132e6827c318c8ec3b379b125ebf0a2a44337f08795d5530937"}, +] + +[package.dependencies] +pydantic = ">=1.9.1" +pyparsing = ">=3.0.9" +python-dateutil = ">=2.8.2" +tzdata = ">=2023.3" + [[package]] name = "identify" version = "2.6.3" @@ -1883,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" @@ -2724,4 +2755,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5836c1a8ad42645d7d045194c8c371754b19957ebdcd2aaa902a2fb3dc97cc53" +content-hash = "7f348f74a05c27e29aaaf25a5584bba9b416f42c3f370db234dd69e5e10dc8df" diff --git a/pyproject.toml b/pyproject.toml index be892cdf..3d761bca 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" +ical = "^8.3.0" [tool.poetry.group.prod.dependencies] # deps used in prod, but unnecessary for development 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") diff --git a/sith/settings.py b/sith/settings.py index 5fdc3786..42e46603 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", @@ -362,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 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/*"] } } }