diff --git a/accounting/admin.py b/accounting/admin.py index 29321713..c3386eb8 100644 --- a/accounting/admin.py +++ b/accounting/admin.py @@ -15,7 +15,16 @@ from django.contrib import admin -from accounting.models import * +from accounting.models import ( + AccountingType, + BankAccount, + ClubAccount, + Company, + GeneralJournal, + Label, + Operation, + SimplifiedAccountingType, +) admin.site.register(BankAccount) admin.site.register(ClubAccount) diff --git a/accounting/models.py b/accounting/models.py index 7eaba943..9b111f61 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -82,9 +82,7 @@ class Company(models.Model): def is_owned_by(self, user): """Check if that object can be edited by the given user.""" - if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): - return True - return False + return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) def can_be_edited_by(self, user): """Check if that object can be edited by the given user.""" @@ -127,9 +125,7 @@ class BankAccount(models.Model): if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True m = self.club.get_membership_for(user) - if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: - return True - return False + return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"] class ClubAccount(models.Model): @@ -161,29 +157,20 @@ class ClubAccount(models.Model): """Check if that object can be edited by the given user.""" if user.is_anonymous: return False - if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): - return True - return False + return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) def can_be_edited_by(self, user): """Check if that object can be edited by the given user.""" m = self.club.get_membership_for(user) - if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: - return True - return False + return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"] def can_be_viewed_by(self, user): """Check if that object can be viewed by the given user.""" m = self.club.get_membership_for(user) - if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: - return True - return False + return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"] def has_open_journal(self): - for j in self.journals.all(): - if not j.closed: - return True - return False + return self.journals.filter(closed=False).exists() def get_open_journal(self): return self.journals.filter(closed=False).first() @@ -228,17 +215,13 @@ class GeneralJournal(models.Model): return False if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True - if self.club_account.can_be_edited_by(user): - return True - return False + return self.club_account.can_be_edited_by(user) def can_be_edited_by(self, user): """Check if that object can be edited by the given user.""" if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True - if self.club_account.can_be_edited_by(user): - return True - return False + return self.club_account.can_be_edited_by(user) def can_be_viewed_by(self, user): return self.club_account.can_be_viewed_by(user) @@ -416,9 +399,7 @@ class Operation(models.Model): if self.journal.closed: return False m = self.journal.club_account.club.get_membership_for(user) - if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: - return True - return False + return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"] def can_be_edited_by(self, user): """Check if that object can be edited by the given user.""" @@ -427,9 +408,7 @@ class Operation(models.Model): if self.journal.closed: return False m = self.journal.club_account.club.get_membership_for(user) - if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: - return True - return False + return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"] class AccountingType(models.Model): @@ -472,9 +451,7 @@ class AccountingType(models.Model): """Check if that object can be edited by the given user.""" if user.is_anonymous: return False - if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): - return True - return False + return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) class SimplifiedAccountingType(models.Model): diff --git a/accounting/tests.py b/accounting/tests.py index 6dc61d08..c66558e0 100644 --- a/accounting/tests.py +++ b/accounting/tests.py @@ -102,7 +102,7 @@ class TestOperation(TestCase): code="443", label="Ce code n'existe pas", movement_type="CREDIT" ) at.save() - l = Label.objects.create(club_account=self.journal.club_account, name="bob") + label = Label.objects.create(club_account=self.journal.club_account, name="bob") self.client.force_login(User.objects.get(username="comptable")) self.op1 = Operation( journal=self.journal, @@ -111,7 +111,7 @@ class TestOperation(TestCase): remark="Test bilan", mode="CASH", done=True, - label=l, + label=label, accounting_type=at, target_type="USER", target_id=self.skia.id, @@ -124,7 +124,7 @@ class TestOperation(TestCase): remark="Test bilan", mode="CASH", done=True, - label=l, + label=label, accounting_type=at, target_type="USER", target_id=self.skia.id, diff --git a/accounting/urls.py b/accounting/urls.py index c8e6082c..f1917462 100644 --- a/accounting/urls.py +++ b/accounting/urls.py @@ -15,7 +15,41 @@ from django.urls import path -from accounting.views import * +from accounting.views import ( + AccountingTypeCreateView, + AccountingTypeEditView, + AccountingTypeListView, + BankAccountCreateView, + BankAccountDeleteView, + BankAccountDetailView, + BankAccountEditView, + BankAccountListView, + ClubAccountCreateView, + ClubAccountDeleteView, + ClubAccountDetailView, + ClubAccountEditView, + CompanyCreateView, + CompanyEditView, + CompanyListView, + JournalAccountingStatementView, + JournalCreateView, + JournalDeleteView, + JournalDetailView, + JournalEditView, + JournalNatureStatementView, + JournalPersonStatementView, + LabelCreateView, + LabelDeleteView, + LabelEditView, + LabelListView, + OperationCreateView, + OperationEditView, + OperationPDFView, + RefoundAccountView, + SimplifiedAccountingTypeCreateView, + SimplifiedAccountingTypeEditView, + SimplifiedAccountingTypeListView, +) urlpatterns = [ # Accounting types diff --git a/accounting/views.py b/accounting/views.py index 276aefcc..dd1cc974 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -182,7 +182,7 @@ class ClubAccountCreateView(CanCreateMixin, CreateView): def get_initial(self): ret = super().get_initial() - if "parent" in self.request.GET.keys(): + if "parent" in self.request.GET: obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first() if obj is not None: ret["bank_account"] = obj.id @@ -264,7 +264,7 @@ class JournalCreateView(CanCreateMixin, CreateView): def get_initial(self): ret = super().get_initial() - if "parent" in self.request.GET.keys(): + if "parent" in self.request.GET: obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first() if obj is not None: ret["club_account"] = obj.id @@ -362,7 +362,7 @@ class OperationForm(forms.ModelForm): def clean(self): self.cleaned_data = super().clean() - if "target_type" in self.cleaned_data.keys(): + if "target_type" in self.cleaned_data: if ( self.cleaned_data.get("user") is None and self.cleaned_data.get("club") is None @@ -633,19 +633,17 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): ret = collections.OrderedDict() statement = collections.OrderedDict() total_sum = 0 - for sat in [None] + list( - SimplifiedAccountingType.objects.order_by("label").all() - ): + for sat in [ + None, + *list(SimplifiedAccountingType.objects.order_by("label")), + ]: amount = queryset.filter( accounting_type__movement_type=movement_type, simpleaccounting_type=sat ).aggregate(amount_sum=Sum("amount"))["amount_sum"] - if sat: - sat = sat.label - else: - sat = "" + label = sat.label if sat is not None else "" if amount: total_sum += amount - statement[sat] = amount + statement[label] = amount ret[movement_type] = statement ret[movement_type + "_sum"] = total_sum return ret @@ -668,15 +666,12 @@ class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView): self.statement(self.object.operations.filter(label=None).all(), "DEBIT") ) statement[_("No label operations")] = no_label_statement - for l in labels: + for label in labels: l_stmt = collections.OrderedDict() - l_stmt.update( - self.statement(self.object.operations.filter(label=l).all(), "CREDIT") - ) - l_stmt.update( - self.statement(self.object.operations.filter(label=l).all(), "DEBIT") - ) - statement[l] = l_stmt + journals = self.object.operations.filter(label=label).all() + l_stmt.update(self.statement(journals, "CREDIT")) + l_stmt.update(self.statement(journals, "DEBIT")) + statement[label] = l_stmt return statement def get_context_data(self, **kwargs): @@ -798,7 +793,7 @@ class LabelCreateView( def get_initial(self): ret = super().get_initial() - if "parent" in self.request.GET.keys(): + if "parent" in self.request.GET: obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first() if obj is not None: ret["club_account"] = obj.id diff --git a/club/forms.py b/club/forms.py index 9b7e831d..a00d2132 100644 --- a/club/forms.py +++ b/club/forms.py @@ -111,8 +111,8 @@ class MailingForm(forms.Form): """Convert given users into real users and check their validity.""" cleaned_data = super().clean() users = [] - for user in cleaned_data["subscription_users"]: - user = User.objects.filter(id=user).first() + for user_id in cleaned_data["subscription_users"]: + user = User.objects.filter(id=user_id).first() if not user: raise forms.ValidationError( _("One of the selected users doesn't exist"), code="invalid" @@ -128,7 +128,7 @@ class MailingForm(forms.Form): def clean(self): cleaned_data = super().clean() - if not "action" in cleaned_data: + if "action" not in cleaned_data: # If there is no action provided, we can stop here raise forms.ValidationError(_("An action is required"), code="invalid") diff --git a/club/models.py b/club/models.py index be875632..573fd176 100644 --- a/club/models.py +++ b/club/models.py @@ -389,9 +389,7 @@ class Membership(models.Model): if user.is_root or user.is_board_member: return True membership = self.club.get_membership_for(user) - if membership is not None and membership.role >= self.role: - return True - return False + return membership is not None and membership.role >= self.role def delete(self, *args, **kwargs): super().delete(*args, **kwargs) diff --git a/club/urls.py b/club/urls.py index a57e36e4..1f82a4d8 100644 --- a/club/urls.py +++ b/club/urls.py @@ -24,7 +24,32 @@ from django.urls import path -from club.views import * +from club.views import ( + ClubCreateView, + ClubEditPropView, + ClubEditView, + ClubListView, + ClubMailingView, + ClubMembersView, + ClubOldMembersView, + ClubPageEditView, + ClubPageHistView, + ClubRevView, + ClubSellingCSVView, + ClubSellingView, + ClubStatView, + ClubToolsView, + ClubView, + MailingAutoGenerationView, + MailingDeleteView, + MailingSubscriptionDeleteView, + MembershipDeleteView, + MembershipSetOldView, + PosterCreateView, + PosterDeleteView, + PosterEditView, + PosterListView, +) urlpatterns = [ path("", ClubListView.as_view(), name="club_list"), @@ -32,32 +57,20 @@ urlpatterns = [ path("stats/", ClubStatView.as_view(), name="club_stats"), path("/", ClubView.as_view(), name="club_view"), path( - "/rev//", - ClubRevView.as_view(), - name="club_view_rev", + "/rev//", ClubRevView.as_view(), name="club_view_rev" ), path("/hist/", ClubPageHistView.as_view(), name="club_hist"), path("/edit/", ClubEditView.as_view(), name="club_edit"), - path( - "/edit/page/", - ClubPageEditView.as_view(), - name="club_edit_page", - ), + path("/edit/page/", ClubPageEditView.as_view(), name="club_edit_page"), path("/members/", ClubMembersView.as_view(), name="club_members"), path( "/elderlies/", ClubOldMembersView.as_view(), name="club_old_members", ), + path("/sellings/", ClubSellingView.as_view(), name="club_sellings"), path( - "/sellings/", - ClubSellingView.as_view(), - name="club_sellings", - ), - path( - "/sellings/csv/", - ClubSellingCSVView.as_view(), - name="sellings_csv", + "/sellings/csv/", ClubSellingCSVView.as_view(), name="sellings_csv" ), path("/prop/", ClubEditPropView.as_view(), name="club_prop"), path("/tools/", ClubToolsView.as_view(), name="tools"), @@ -89,9 +102,7 @@ urlpatterns = [ ), path("/poster/", PosterListView.as_view(), name="poster_list"), path( - "/poster/create/", - PosterCreateView.as_view(), - name="poster_create", + "/poster/create/", PosterCreateView.as_view(), name="poster_create" ), path( "/poster//edit/", diff --git a/club/views.py b/club/views.py index 089256d6..e1f3367e 100644 --- a/club/views.py +++ b/club/views.py @@ -397,7 +397,8 @@ class ClubSellingCSVView(ClubSellingView): row.append(selling.customer.user.get_display_name()) else: row.append("") - row = row + [ + row = [ + *row, selling.label, selling.quantity, selling.quantity * selling.unit_price, @@ -408,7 +409,7 @@ class ClubSellingCSVView(ClubSellingView): row.append(selling.product.purchase_price) row.append(selling.product.selling_price - selling.product.purchase_price) else: - row = row + ["", "", ""] + row = [*row, "", "", ""] return row def get(self, request, *args, **kwargs): @@ -622,9 +623,7 @@ class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): def remove_subscription(self, cleaned_data): """Remove specified users from a mailing list.""" fields = [ - cleaned_data[key] - for key in cleaned_data.keys() - if key.startswith("removal_") + val for key, val in cleaned_data.items() if key.startswith("removal_") ] for field in fields: for sub in field: diff --git a/com/admin.py b/com/admin.py index 84e85328..21e14e4f 100644 --- a/com/admin.py +++ b/com/admin.py @@ -15,7 +15,7 @@ from django.contrib import admin from haystack.admin import SearchModelAdmin -from com.models import * +from com.models import News, Poster, Screen, Sith, Weekmail @admin.register(News) diff --git a/com/urls.py b/com/urls.py index b0376936..304e5312 100644 --- a/com/urls.py +++ b/com/urls.py @@ -16,7 +16,36 @@ from django.urls import path from club.views import MailingDeleteView -from com.views import * +from com.views import ( + AlertMsgEditView, + InfoMsgEditView, + MailingListAdminView, + MailingModerateView, + NewsAdminListView, + NewsCreateView, + NewsDeleteView, + NewsDetailView, + NewsEditView, + NewsListView, + NewsModerateView, + PosterCreateView, + PosterDeleteView, + PosterEditView, + PosterListView, + PosterModerateListView, + PosterModerateView, + ScreenCreateView, + ScreenDeleteView, + ScreenEditView, + ScreenListView, + ScreenSlideshowView, + WeekmailArticleCreateView, + WeekmailArticleDeleteView, + WeekmailArticleEditView, + WeekmailDestinationEditView, + WeekmailEditView, + WeekmailPreviewView, +) urlpatterns = [ path("sith/edit/alert/", AlertMsgEditView.as_view(), name="alert_edit"), @@ -46,15 +75,9 @@ urlpatterns = [ path("news/", NewsListView.as_view(), name="news_list"), path("news/admin/", NewsAdminListView.as_view(), name="news_admin_list"), path("news/create/", NewsCreateView.as_view(), name="news_new"), + path("news//delete/", NewsDeleteView.as_view(), name="news_delete"), path( - "news//delete/", - NewsDeleteView.as_view(), - name="news_delete", - ), - path( - "news//moderate/", - NewsModerateView.as_view(), - name="news_moderate", + "news//moderate/", NewsModerateView.as_view(), name="news_moderate" ), path("news//edit/", NewsEditView.as_view(), name="news_edit"), path("news//", NewsDetailView.as_view(), name="news_detail"), @@ -71,11 +94,7 @@ urlpatterns = [ ), path("poster/", PosterListView.as_view(), name="poster_list"), path("poster/create/", PosterCreateView.as_view(), name="poster_create"), - path( - "poster//edit/", - PosterEditView.as_view(), - name="poster_edit", - ), + path("poster//edit/", PosterEditView.as_view(), name="poster_edit"), path( "poster//delete/", PosterDeleteView.as_view(), @@ -98,11 +117,7 @@ urlpatterns = [ ScreenSlideshowView.as_view(), name="screen_slideshow", ), - path( - "screen//edit/", - ScreenEditView.as_view(), - name="screen_edit", - ), + path("screen//edit/", ScreenEditView.as_view(), name="screen_edit"), path( "screen//delete/", ScreenDeleteView.as_view(), diff --git a/com/views.py b/com/views.py index 97f2d080..f9ea78eb 100644 --- a/com/views.py +++ b/com/views.py @@ -86,12 +86,11 @@ class PosterForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) super().__init__(*args, **kwargs) - if self.user: - if not self.user.is_com_admin: - self.fields["club"].queryset = Club.objects.filter( - id__in=self.user.clubs_with_rights - ) - self.fields.pop("display_time") + if self.user and not self.user.is_com_admin: + self.fields["club"].queryset = Club.objects.filter( + id__in=self.user.clubs_with_rights + ) + self.fields.pop("display_time") class ComTabsMixin(TabedViewMixin): @@ -312,7 +311,7 @@ class NewsCreateView(CanCreateMixin, CreateView): def post(self, request, *args, **kwargs): form = self.get_form() - if form.is_valid() and "preview" not in request.POST.keys(): + if form.is_valid() and "preview" not in request.POST: return self.form_valid(form) else: self.object = form.instance @@ -354,13 +353,13 @@ class NewsModerateView(CanEditMixin, SingleObjectMixin): def get(self, request, *args, **kwargs): self.object = self.get_object() - if "remove" in request.GET.keys(): + if "remove" in request.GET: self.object.is_moderated = False else: self.object.is_moderated = True self.object.moderator = request.user self.object.save() - if "next" in self.request.GET.keys(): + if "next" in self.request.GET: return redirect(self.request.GET["next"]) return redirect("com:news_admin_list") @@ -424,7 +423,7 @@ class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, Detai try: self.object.send() # This should fail except SMTPRecipientsRefused as e: - users = User.objects.filter(email__in=e.recipients.keys()) + users = User.objects.filter(email__in=e.recipients) for u in users: u.preferences.receive_weekmail = False u.preferences.save() @@ -471,7 +470,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi def get(self, request, *args, **kwargs): self.object = self.get_object() - if "up_article" in request.GET.keys(): + if "up_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["up_article"], weekmail=self.object ) @@ -483,7 +482,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi art.save() prev_art.save() self.quick_notif_list += ["qn_success"] - if "down_article" in request.GET.keys(): + if "down_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["down_article"], weekmail=self.object ) @@ -495,7 +494,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi art.save() next_art.save() self.quick_notif_list += ["qn_success"] - if "add_article" in request.GET.keys(): + if "add_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["add_article"], weekmail=None ) @@ -504,7 +503,7 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi art.rank += 1 art.save() self.quick_notif_list += ["qn_success"] - if "del_article" in request.GET.keys(): + if "del_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["del_article"], weekmail=self.object ) @@ -571,7 +570,7 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView): ) ), ) - if form.is_valid() and not "preview" in request.POST.keys(): + if form.is_valid() and "preview" not in request.POST: return self.form_valid(form) else: return self.form_invalid(form) @@ -689,19 +688,13 @@ class PosterEditBaseView(UpdateView): template_name = "com/poster_edit.jinja" def get_initial(self): - init = {} - try: - init["date_begin"] = self.object.date_begin.strftime("%Y-%m-%d %H:%M:%S") - except Exception: - pass - try: - init["date_end"] = self.object.date_end.strftime("%Y-%m-%d %H:%M:%S") - except Exception: - pass - return init + 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"), + } def dispatch(self, request, *args, **kwargs): - if "club_id" in kwargs and kwargs["club_id"]: + if kwargs.get("club_id"): try: self.club = Club.objects.get(pk=kwargs["club_id"]) except Club.DoesNotExist as e: @@ -737,7 +730,7 @@ class PosterDeleteBaseView(DeleteView): template_name = "core/delete_confirm.jinja" def dispatch(self, request, *args, **kwargs): - if "club_id" in kwargs and kwargs["club_id"]: + if kwargs.get("club_id"): try: self.club = Club.objects.get(pk=kwargs["club_id"]) except Club.DoesNotExist as e: diff --git a/core/management/commands/install_xapian.py b/core/management/commands/install_xapian.py index fe7b6994..4be1d907 100644 --- a/core/management/commands/install_xapian.py +++ b/core/management/commands/install_xapian.py @@ -67,5 +67,6 @@ class Command(BaseCommand): subprocess.run( [str(Path(__file__).parent / "install_xapian.sh"), desired], env=dict(os.environ), - ).check_returncode() + check=True, + ) self.stdout.write("Installation success") diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 301b1228..18a524b2 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -934,7 +934,7 @@ Welcome to the wiki page! # Adding subscription for sli s = Subscription( member=User.objects.filter(pk=sli.pk).first(), - subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], + subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())), payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], ) s.subscription_start = s.compute_start() @@ -947,7 +947,7 @@ Welcome to the wiki page! # Adding subscription for Krophil s = Subscription( member=User.objects.filter(pk=krophil.pk).first(), - subscription_type=list(settings.SITH_SUBSCRIPTIONS.keys())[0], + subscription_type=next(iter(settings.SITH_SUBSCRIPTIONS.keys())), payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0], ) s.subscription_start = s.compute_start() diff --git a/core/management/commands/populate_more.py b/core/management/commands/populate_more.py index f3582bae..50a0052f 100644 --- a/core/management/commands/populate_more.py +++ b/core/management/commands/populate_more.py @@ -217,9 +217,9 @@ class Command(BaseCommand): UV.objects.bulk_create(uvs, ignore_conflicts=True) def create_products(self): - categories = [] - for _ in range(10): - categories.append(ProductType(name=self.faker.text(max_nb_chars=30))) + categories = [ + ProductType(name=self.faker.text(max_nb_chars=30)) for _ in range(10) + ] ProductType.objects.bulk_create(categories) categories = list( ProductType.objects.filter(name__in=[c.name for c in categories]) @@ -254,16 +254,16 @@ class Command(BaseCommand): archived=bool(random.random() > 0.7), ) products.append(product) - for group in random.sample(groups, k=random.randint(0, 3)): - # there will be products without buying groups - # but there are also such products in the real database - buying_groups.append( - Product.buying_groups.through(product=product, group=group) - ) - for counter in random.sample(counters, random.randint(0, 4)): - selling_places.append( - Counter.products.through(counter=counter, product=product) - ) + # there will be products without buying groups + # but there are also such products in the real database + buying_groups.extend( + Product.buying_groups.through(product=product, group=group) + for group in random.sample(groups, k=random.randint(0, 3)) + ) + selling_places.extend( + Counter.products.through(counter=counter, product=product) + for counter in random.sample(counters, random.randint(0, 4)) + ) Product.objects.bulk_create(products) Product.buying_groups.through.objects.bulk_create(buying_groups) Counter.products.through.objects.bulk_create(selling_places) diff --git a/core/models.py b/core/models.py index 36d0902d..adbe56c4 100644 --- a/core/models.py +++ b/core/models.py @@ -174,7 +174,7 @@ def validate_promo(value: int) -> None: ) -def get_group(*, pk: int = None, name: str = None) -> Group | None: +def get_group(*, pk: int | None = None, name: str | None = None) -> Group | None: """Search for a group by its primary key or its name. Either one of the two must be set. @@ -445,7 +445,7 @@ class User(AbstractBaseUser): else: return 0 - def is_in_group(self, *, pk: int = None, name: str = None) -> bool: + def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool: """Check if this user is in the given group. Either a group id or a group name must be provided. If both are passed, only the id will be considered. @@ -649,7 +649,7 @@ class User(AbstractBaseUser): continue links = list(User.godfathers.through.objects.filter(**{key: self.id})) res.extend(links) - for _ in range(1, depth): + for _ in range(1, depth): # noqa: F402 we don't care about gettext here ids = [getattr(c, reverse_key) for c in links] links = list( User.godfathers.through.objects.filter( @@ -703,9 +703,7 @@ class User(AbstractBaseUser): return True if hasattr(obj, "owner_group") and self.is_in_group(pk=obj.owner_group.id): return True - if self.is_root: - return True - return False + return self.is_root def can_edit(self, obj): """Determine if the object can be edited by the user.""" @@ -717,9 +715,7 @@ class User(AbstractBaseUser): return True if isinstance(obj, User) and obj == self: return True - if self.is_owner(obj): - return True - return False + return self.is_owner(obj) def can_view(self, obj): """Determine if the object can be viewed by the user.""" @@ -729,9 +725,7 @@ class User(AbstractBaseUser): for pk in obj.view_groups.values_list("pk", flat=True): if self.is_in_group(pk=pk): return True - if self.can_edit(obj): - return True - return False + return self.can_edit(obj) def can_be_edited_by(self, user): return user.is_root or user.is_board_member @@ -759,23 +753,17 @@ class User(AbstractBaseUser): @cached_property def preferences(self): - try: + if hasattr(self, "_preferences"): return self._preferences - except: - prefs = Preferences(user=self) - prefs.save() - return prefs + return Preferences.objects.create(user=self) @cached_property def forum_infos(self): - try: + if hasattr(self, "_forum_infos"): return self._forum_infos - except: - from forum.models import ForumUserInfo + from forum.models import ForumUserInfo - infos = ForumUserInfo(user=self) - infos.save() - return infos + return ForumUserInfo.objects.create(user=self) @cached_property def clubs_with_rights(self) -> list[Club]: @@ -840,7 +828,7 @@ class AnonymousUser(AuthAnonymousUser): def favorite_topics(self): raise PermissionDenied - def is_in_group(self, *, pk: int = None, name: str = None) -> bool: + def is_in_group(self, *, pk: int | None = None, name: str | None = None) -> bool: """The anonymous user is only in the public group.""" allowed_id = settings.SITH_GROUP_PUBLIC_ID if pk is not None: @@ -867,9 +855,7 @@ class AnonymousUser(AuthAnonymousUser): and obj.view_groups.filter(id=settings.SITH_GROUP_PUBLIC_ID).exists() ): return True - if hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self): - return True - return False + return hasattr(obj, "can_be_viewed_by") and obj.can_be_viewed_by(self) def get_display_name(self): return _("Visitor") @@ -1070,7 +1056,7 @@ class SithFile(models.Model): ]: self.file.delete() self.file = None - except: + except: # noqa E722 I don't know the exception that can be raised self.file = None self.mime_type = "inode/directory" if self.is_file and (self.file is None or self.file == ""): @@ -1196,12 +1182,12 @@ class SithFile(models.Model): return Album.objects.filter(id=self.id).first() def get_parent_list(self): - l = [] - p = self.parent - while p is not None: - l.append(p) - p = p.parent - return l + parents = [] + current = self.parent + while current is not None: + parents.append(current) + current = current.parent + return parents def get_parent_path(self): return "/" + "/".join([p.name for p in self.get_parent_list()[::-1]]) @@ -1359,22 +1345,18 @@ class Page(models.Model): if hasattr(self, "club") and self.club.can_be_edited_by(user): # Override normal behavior for clubs return True - if self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member: - return True - return False + return self.name == settings.SITH_CLUB_ROOT_PAGE and user.is_board_member def can_be_viewed_by(self, user): - if self.is_club_page: - return True - return False + return self.is_club_page def get_parent_list(self): - l = [] - p = self.parent - while p is not None: - l.append(p) - p = p.parent - return l + parents = [] + current = self.parent + while current is not None: + parents.append(current) + current = current.parent + return parents def is_locked(self): """Is True if the page is locked, False otherwise. @@ -1386,7 +1368,6 @@ class Page(models.Model): if self.lock_timeout and ( timezone.now() - self.lock_timeout > timedelta(minutes=5) ): - # print("Lock timed out") self.unset_lock() return ( self.lock_user @@ -1401,7 +1382,6 @@ class Page(models.Model): self.lock_user = user self.lock_timeout = timezone.now() super().save() - # print("Locking page") def set_lock_recursive(self, user): """Locks recursively all the child pages for editing properties.""" @@ -1420,7 +1400,6 @@ class Page(models.Model): self.lock_user = None self.lock_timeout = None super().save() - # print("Unlocking page") def get_lock(self): """Returns the page's mutex containing the time and the user in a dict.""" @@ -1435,13 +1414,11 @@ class Page(models.Model): """ if self.parent is None: return self.name - return "/".join([self.parent.get_full_name(), self.name]) + return f"{self.parent.get_full_name()}/{self.name}" def get_display_name(self): - try: - return self.revisions.last().title - except: - return self.name + rev = self.revisions.last() + return rev.title if rev is not None else self.name @cached_property def is_club_page(self): diff --git a/core/static/core/style.scss b/core/static/core/style.scss index f0ec9ac6..d658d957 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -28,6 +28,7 @@ input[type="file"] { font-size: 1.2em; border-radius: 5px; color: black; + &:hover { background: hsl(0, 0%, 83%); } @@ -63,6 +64,7 @@ textarea[type="text"], border-radius: 5px; max-width: 95%; } + textarea { border: none; text-decoration: none; @@ -72,6 +74,7 @@ textarea { border-radius: 5px; font-family: sans-serif; } + select { border: none; text-decoration: none; @@ -85,9 +88,11 @@ select { a:not(.button) { text-decoration: none; color: $primary-dark-color; + &:hover { color: $primary-light-color; } + &:active { color: $primary-color; } @@ -116,7 +121,9 @@ a:not(.button) { } @keyframes rotate { - 100% { transform: rotate(360deg); } + 100% { + transform: rotate(360deg); + } } .ib { @@ -143,11 +150,13 @@ a:not(.button) { .collapse-header-icon { transition: all ease-in-out 150ms; + &.reverse { transform: rotate(180deg); } } } + .collapse-body { padding: 10px; } @@ -202,9 +211,11 @@ a:not(.button) { font-size: 0.9em; margin: 0.2em; border-radius: 0.6em; + .markdown { margin: 0.5em; } + &:before { font-family: FontAwesome; font-size: 4em; @@ -212,15 +223,19 @@ a:not(.button) { margin: 0.2em; } } + #info_box { background: $primary-neutral-light-color; + &:before { content: "\f05a"; color: hsl(210, 100%, 56%); } } + #alert_box { background: $second-color; + &:before { content: "\f06a"; color: $white-color; @@ -240,7 +255,8 @@ a:not(.button) { #page { width: 90%; margin: 20px auto 0; -/*---------------------------------NAV---------------------------------*/ + + /*---------------------------------NAV---------------------------------*/ .btn { font-size: 15px; font-weight: normal; @@ -252,9 +268,11 @@ a:not(.button) { &.btn-blue { background-color: $deepblue; + &:not(:disabled):hover { background-color: darken($deepblue, 10%); } + &:disabled { background-color: rgba(70, 90, 126, 0.4); } @@ -262,9 +280,11 @@ a:not(.button) { &.btn-grey { background-color: grey; + &:not(:disabled):hover { background-color: darken(gray, 15%); } + &:disabled { background-color: lighten(gray, 15%); } @@ -273,9 +293,11 @@ a:not(.button) { &.btn-red { background-color: #fc8181; color: black; + &:not(:disabled):hover { background-color: darken(#fc8181, 15%); } + &:disabled { background-color: lighten(#fc8181, 15%); color: grey; @@ -287,12 +309,13 @@ a:not(.button) { } } -/*--------------------------------CONTENT------------------------------*/ + /*--------------------------------CONTENT------------------------------*/ #quick_notif { width: 100%; margin: 0 auto; list-style-type: none; background: $second-color; + li { padding: 10px; } @@ -333,14 +356,24 @@ a:not(.button) { border: #fc8181 1px solid; } + .alert-title { + margin-top: 0; + } + .alert-main { flex: 2; } + + .alert-aside { + display: flex; + flex-direction: column; + } } .tool_bar { overflow: auto; padding: 4px; + .tools { display: flex; flex-wrap: wrap; @@ -349,6 +382,7 @@ a:not(.button) { padding: 5px; border-radius: 6px; text-align: center; + a { padding: 7px; display: inline-block; @@ -358,11 +392,13 @@ a:not(.button) { flex: 1; flex-wrap: nowrap; white-space: nowrap; + &.selected_tab { background: $primary-color; color: $white-color; border-radius: 6px; } + &:hover { background: $primary-color; color: $white-color; @@ -372,7 +408,7 @@ a:not(.button) { } } -/*---------------------------------NEWS--------------------------------*/ + /*---------------------------------NEWS--------------------------------*/ #news { display: flex; @@ -385,17 +421,21 @@ a:not(.button) { 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; @@ -403,19 +443,22 @@ a:not(.button) { 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 */ #agenda, #birthdays { display: block; @@ -423,6 +466,7 @@ a:not(.button) { background: white; font-size: 70%; margin-bottom: 1em; + #agenda_title, #birthdays_title { margin: 0; @@ -435,39 +479,48 @@ a:not(.button) { 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 { + + >li { padding: 0.5em; + &:nth-child(even) { background: $secondary-neutral-light-color; } } + ul { margin: 0; margin-left: 1em; @@ -478,13 +531,15 @@ a:not(.button) { } } } -/* END AGENDA/BIRTHDAYS */ -/* EVENTS TODAY AND NEXT FEW DAYS */ + /* 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; @@ -500,33 +555,42 @@ a:not(.button) { 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; @@ -534,6 +598,7 @@ a:not(.button) { margin: 0; margin-right: 1em; margin-top: 0.8em; + img { max-height: 6em; max-width: 8em; @@ -541,16 +606,21 @@ a:not(.button) { margin: 0 auto; } } + .news_date { font-size: 100%; } + .news_content { clear: left; + .button_bar { text-align: right; + .fb { color: $faceblue; } + .twitter { color: $twitblue; } @@ -559,26 +629,30 @@ a:not(.button) { } } } -/* END EVENTS TODAY AND NEXT FEW DAYS */ -/* COMING SOON */ + /* 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 */ + /* END COMING SOON */ + + /* NOTICES */ .news_notice { margin: 0 0 1em 1em; padding: 0.4em; @@ -586,16 +660,19 @@ a:not(.button) { 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 */ + /* END NOTICES */ + + /* CALLS */ .news_call { margin: 0 0 1em 1em; padding: 0.4em; @@ -603,21 +680,26 @@ a:not(.button) { 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 */ + + /* END CALLS */ .news_empty { margin-left: 1em; } + .news_date { color: grey; } @@ -631,8 +713,8 @@ a:not(.button) { } -.select2 { - margin: 10px 0!important; +.tomselected { + margin: 10px 0 !important; max-width: 100%; min-width: 100%; @@ -648,7 +730,9 @@ a:not(.button) { color: black; } } -.select2-results { + +.ts-dropdown { + .select-item { display: flex; flex-direction: row; @@ -664,16 +748,39 @@ a:not(.button) { } } +.ts-control { + + .item { + .fa-times { + margin-left: 5px; + margin-right: 5px; + } + + cursor: pointer; + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + display: inline-block; + margin-left: 5px; + margin-top: 5px; + margin-bottom: 5px; + padding-right: 10px; + } + +} + #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; @@ -681,6 +788,7 @@ a:not(.button) { float: left; min-width: 15em; margin: 0; + img { max-height: 15em; max-width: 12em; @@ -689,6 +797,7 @@ a:not(.button) { margin-bottom: 10px; } } + .share_button { border: none; color: white; @@ -700,6 +809,7 @@ a:not(.button) { float: right; display: block; margin-left: 0.3em; + &:hover { color: lightgrey; } @@ -731,26 +841,32 @@ a:not(.button) { #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; @@ -759,27 +875,32 @@ a:not(.button) { 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%; @@ -791,26 +912,31 @@ a:not(.button) { 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; @@ -829,10 +955,12 @@ a:not(.button) { } } } + .dates { padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid whitesmoke; + * { display: flex; justify-content: center; @@ -841,15 +969,18 @@ a:not(.button) { margin-left: 5px; margin-right: 5px; } + .begin, .end { width: 48%; } + .begin { border-right: 1px solid whitesmoke; padding-right: 2%; } } + .edit, .moderate, .slideshow { @@ -857,15 +988,18 @@ a:not(.button) { 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; @@ -876,23 +1010,28 @@ a:not(.button) { 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; @@ -906,9 +1045,11 @@ a:not(.button) { visibility: hidden; background-color: rgba(10, 10, 10, 0.9); overflow: hidden; + &.active { visibility: visible; } + #placeholder { width: 80vw; height: 80vh; @@ -917,6 +1058,7 @@ a:not(.button) { align-items: center; top: 0; left: 0; + img { max-width: 100%; max-height: 100%; @@ -931,14 +1073,17 @@ a:not(.button) { tbody { .neg-amount { color: red; + &:before { font-family: FontAwesome; font-size: 1em; content: "\f063"; } } + .pos-amount { color: green; + &:before { font-family: FontAwesome; font-size: 1em; @@ -1005,6 +1150,7 @@ dt { .edit-bar { display: block; margin: 4px; + a { display: inline-block; margin: 4px; @@ -1044,7 +1190,8 @@ th { vertical-align: middle; text-align: center; padding: 5px 10px; - > ul { + + >ul { margin-top: 0; } } @@ -1055,7 +1202,8 @@ td { vertical-align: top; overflow: hidden; text-overflow: ellipsis; - > ul { + + >ul { margin-top: 0; } } @@ -1071,15 +1219,17 @@ thead { color: white; } -tbody > tr { +tbody>tr { &:nth-child(even):not(.highlight) { background: $primary-neutral-light-color; } + &.clickable:hover { cursor: pointer; background: $secondary-neutral-light-color; width: 100%; } + &.highlight { color: $primary-dark-color; font-style: italic; @@ -1139,9 +1289,11 @@ u, margin: 0.2em; height: 100%; background: $secondary-neutral-light-color; + img { max-width: 70%; } + input { background: white; } @@ -1153,10 +1305,12 @@ u, .user_mini_profile { height: 100%; width: 100%; + img { max-width: 100%; max-height: 100%; } + .user_mini_profile_infos { padding: 0.2em; height: 20%; @@ -1164,16 +1318,20 @@ u, flex-wrap: nowrap; justify-content: space-around; font-size: 0.9em; + div { max-height: 100%; } + .user_mini_profile_infos_text { text-align: center; + .user_mini_profile_nick { font-style: italic; } } } + .user_mini_profile_picture { height: 80%; display: flex; @@ -1185,14 +1343,17 @@ u, .mini_profile_link { display: block; text-decoration: none; + span { display: inline-block; width: 50px; vertical-align: middle; } + em { vertical-align: middle; } + img { max-width: 40px; max-height: 60px; @@ -1214,6 +1375,7 @@ u, border: solid 1px red; text-align: center; } + img { width: 500px; } @@ -1223,6 +1385,7 @@ u, .matmat_results { display: flex; flex-wrap: wrap; + .matmat_user { flex-basis: 14em; align-self: flex-start; @@ -1231,10 +1394,12 @@ u, overflow: hidden; border: 1px solid black; box-shadow: $shadow-color 1px 1px 1px; + &:hover { box-shadow: 1px 1px 5px $second-color; } } + .matmat_user a { color: $primary-neutral-dark-color; height: 100%; @@ -1274,6 +1439,7 @@ footer { font-size: 90%; text-align: center; vertical-align: middle; + div { margin: 0.6em 0; color: $white-color; @@ -1283,18 +1449,20 @@ footer { align-items: center; background-color: $primary-neutral-dark-color; box-shadow: $shadow-color 0 0 15px; + a { padding: 0.8em; flex: 1; font-weight: bold; color: $white-color !important; + &:hover { color: $primary-dark-color; } } } - > .version { + >.version { margin-top: 3px; color: rgba(0, 0, 0, 0.3); } @@ -1326,6 +1494,7 @@ label { * { text-align: center; } + img { width: 100px; } @@ -1342,19 +1511,23 @@ label { padding: 2px; display: inline-block; font-size: 0.8em; + span { width: 70px; float: right; } + img { max-width: 50px; max-height: 50px; float: left; } + strong { font-weight: bold; font-size: 1.2em; } + button { vertical-align: middle; } @@ -1371,6 +1544,7 @@ a.ui-button:active, background: $primary-color; border-color: $primary-color; } + .ui-corner-all, .ui-corner-bottom, .ui-corner-right, @@ -1382,10 +1556,11 @@ a.ui-button:active, #club_detail { .club_logo { float: right; + img { display: block; max-height: 10em; max-width: 10em; } } -} +} \ No newline at end of file diff --git a/core/static/webpack/ajax-select-index.ts b/core/static/webpack/ajax-select-index.ts new file mode 100644 index 00000000..e64df216 --- /dev/null +++ b/core/static/webpack/ajax-select-index.ts @@ -0,0 +1,93 @@ +import "tom-select/dist/css/tom-select.css"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; +import TomSelect from "tom-select"; +import type { TomItem, TomLoadCallback, TomOption } from "tom-select/dist/types/types"; +import type { escape_html } from "tom-select/dist/types/utils"; +import { type UserProfileSchema, userSearchUsers } from "#openapi"; + +@registerComponent("ajax-select") +export class AjaxSelect extends inheritHtmlElement("select") { + public widget: TomSelect; + public filter?: (items: T[]) => T[]; + + constructor() { + super(); + + window.addEventListener("DOMContentLoaded", () => { + this.loadTomSelect(); + }); + } + + loadTomSelect() { + const minCharNumberForSearch = 2; + let maxItems = 1; + + if (this.node.multiple) { + maxItems = Number.parseInt(this.node.dataset.max) ?? null; + } + + this.widget = new TomSelect(this.node, { + hideSelected: true, + diacritics: true, + duplicates: false, + maxItems: maxItems, + loadThrottle: Number.parseInt(this.node.dataset.delay) ?? null, + valueField: "id", + labelField: "display_name", + searchField: ["display_name", "nick_name", "first_name", "last_name"], + placeholder: this.node.dataset.placeholder ?? "", + shouldLoad: (query: string) => { + return query.length >= minCharNumberForSearch; // Avoid launching search with less than 2 characters + }, + load: (query: string, callback: TomLoadCallback) => { + userSearchUsers({ + query: { + search: query, + }, + }).then((response) => { + if (response.data) { + if (this.filter) { + callback(this.filter(response.data.results), []); + } else { + callback(response.data.results, []); + } + return; + } + callback([], []); + }); + }, + render: { + option: (item: UserProfileSchema, sanitize: typeof escape_html) => { + return `
+ ${sanitize(item.display_name)} + ${sanitize(item.display_name)} +
`; + }, + item: (item: UserProfileSchema, sanitize: typeof escape_html) => { + return `${sanitize(item.display_name)}`; + }, + // biome-ignore lint/style/useNamingConvention: that's how it's defined + not_loading: (data: TomOption, _sanitize: typeof escape_html) => { + return `
${interpolate(gettext("You need to type %(number)s more characters"), { number: minCharNumberForSearch - data.input.length }, true)}
`; + }, + // biome-ignore lint/style/useNamingConvention: that's how it's defined + no_results: (_data: TomOption, _sanitize: typeof escape_html) => { + return `
${gettext("No results found")}
`; + }, + }, + }); + + // Allow removing selected items by clicking on them + this.widget.on("item_select", (item: TomItem) => { + this.widget.removeItem(item); + }); + // Remove typed text once an item has been selected + this.widget.on("item_add", () => { + this.widget.setTextboxValue(""); + }); + } +} diff --git a/core/static/webpack/easymde-index.js b/core/static/webpack/easymde-index.ts similarity index 63% rename from core/static/webpack/easymde-index.js rename to core/static/webpack/easymde-index.ts index 378c1303..04a33f17 100644 --- a/core/static/webpack/easymde-index.js +++ b/core/static/webpack/easymde-index.ts @@ -1,53 +1,57 @@ // biome-ignore lint/correctness/noUndeclaredDependencies: shipped by easymde import "codemirror/lib/codemirror.css"; import "easymde/src/css/easymde.css"; -import easyMde from "easymde"; +import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; +// biome-ignore lint/correctness/noUndeclaredDependencies: Imported by EasyMDE +import type CodeMirror from "codemirror"; +// biome-ignore lint/style/useNamingConvention: This is how they called their namespace +import EasyMDE from "easymde"; import { markdownRenderMarkdown } from "#openapi"; -/** - * Create a new easymde based textarea - * @param {HTMLTextAreaElement} textarea to use - **/ -window.easymdeFactory = (textarea) => { - const easymde = new easyMde({ +const loadEasyMde = (textarea: HTMLTextAreaElement) => { + new EasyMDE({ element: textarea, spellChecker: false, autoDownloadFontAwesome: false, - previewRender: Alpine.debounce(async (plainText, preview) => { - preview.innerHTML = ( - await markdownRenderMarkdown({ body: { text: plainText } }) - ).data; + previewRender: Alpine.debounce((plainText: string, preview: MarkdownInput) => { + const func = async (plainText: string, preview: MarkdownInput): Promise => { + preview.innerHTML = ( + await markdownRenderMarkdown({ body: { text: plainText } }) + ).data as string; + return null; + }; + func(plainText, preview); return null; }, 300), forceSync: true, // Avoid validation error on generic create view toolbar: [ { name: "heading-smaller", - action: easyMde.toggleHeadingSmaller, + action: EasyMDE.toggleHeadingSmaller, className: "fa fa-header", title: gettext("Heading"), }, { name: "italic", - action: easyMde.toggleItalic, + action: EasyMDE.toggleItalic, className: "fa fa-italic", title: gettext("Italic"), }, { name: "bold", - action: easyMde.toggleBold, + action: EasyMDE.toggleBold, className: "fa fa-bold", title: gettext("Bold"), }, { name: "strikethrough", - action: easyMde.toggleStrikethrough, + action: EasyMDE.toggleStrikethrough, className: "fa fa-strikethrough", title: gettext("Strikethrough"), }, { name: "underline", - action: function customFunction(editor) { + action: function customFunction(editor: { codemirror: CodeMirror.Editor }) { const cm = editor.codemirror; cm.replaceSelection(`__${cm.getSelection()}__`); }, @@ -56,7 +60,7 @@ window.easymdeFactory = (textarea) => { }, { name: "superscript", - action: function customFunction(editor) { + action: function customFunction(editor: { codemirror: CodeMirror.Editor }) { const cm = editor.codemirror; cm.replaceSelection(`^${cm.getSelection()}^`); }, @@ -65,7 +69,7 @@ window.easymdeFactory = (textarea) => { }, { name: "subscript", - action: function customFunction(editor) { + action: function customFunction(editor: { codemirror: CodeMirror.Editor }) { const cm = editor.codemirror; cm.replaceSelection(`~${cm.getSelection()}~`); }, @@ -74,71 +78,71 @@ window.easymdeFactory = (textarea) => { }, { name: "code", - action: easyMde.toggleCodeBlock, + action: EasyMDE.toggleCodeBlock, className: "fa fa-code", title: gettext("Code"), }, "|", { name: "quote", - action: easyMde.toggleBlockquote, + action: EasyMDE.toggleBlockquote, className: "fa fa-quote-left", title: gettext("Quote"), }, { name: "unordered-list", - action: easyMde.toggleUnorderedList, + action: EasyMDE.toggleUnorderedList, className: "fa fa-list-ul", title: gettext("Unordered list"), }, { name: "ordered-list", - action: easyMde.toggleOrderedList, + action: EasyMDE.toggleOrderedList, className: "fa fa-list-ol", title: gettext("Ordered list"), }, "|", { name: "link", - action: easyMde.drawLink, + action: EasyMDE.drawLink, className: "fa fa-link", title: gettext("Insert link"), }, { name: "image", - action: easyMde.drawImage, + action: EasyMDE.drawImage, className: "fa-regular fa-image", title: gettext("Insert image"), }, { name: "table", - action: easyMde.drawTable, + action: EasyMDE.drawTable, className: "fa fa-table", title: gettext("Insert table"), }, "|", { name: "clean-block", - action: easyMde.cleanBlock, + action: EasyMDE.cleanBlock, className: "fa fa-eraser fa-clean-block", title: gettext("Clean block"), }, "|", { name: "preview", - action: easyMde.togglePreview, + action: EasyMDE.togglePreview, className: "fa fa-eye no-disable", title: gettext("Toggle preview"), }, { name: "side-by-side", - action: easyMde.toggleSideBySide, + action: EasyMDE.toggleSideBySide, className: "fa fa-columns no-disable no-mobile", title: gettext("Toggle side by side"), }, { name: "fullscreen", - action: easyMde.toggleFullScreen, + action: EasyMDE.toggleFullScreen, className: "fa fa-expand no-mobile", title: gettext("Toggle fullscreen"), }, @@ -152,27 +156,25 @@ window.easymdeFactory = (textarea) => { ], }); - const submits = textarea.closest("form").querySelectorAll('input[type="submit"]'); - const parentDiv = textarea.parentElement; - let submitPressed = false; + const submits: HTMLInputElement[] = Array.from( + textarea.closest("form").querySelectorAll('input[type="submit"]'), + ); + const parentDiv = textarea.parentElement.parentElement; - function checkMarkdownInput() { + function checkMarkdownInput(event: Event) { // an attribute is null if it does not exist, else a string const required = textarea.getAttribute("required") != null; const length = textarea.value.trim().length; if (required && length === 0) { parentDiv.style.boxShadow = "red 0px 0px 1.5px 1px"; + event.preventDefault(); } else { parentDiv.style.boxShadow = ""; } } - function onSubmitClick(e) { - if (!submitPressed) { - easymde.codemirror.on("change", checkMarkdownInput); - } - submitPressed = true; + function onSubmitClick(e: Event) { checkMarkdownInput(e); } @@ -180,3 +182,11 @@ window.easymdeFactory = (textarea) => { submit.addEventListener("click", onSubmitClick); } }; + +@registerComponent("markdown-input") +class MarkdownInput extends inheritHtmlElement("textarea") { + constructor() { + super(); + window.addEventListener("DOMContentLoaded", () => loadEasyMde(this.node)); + } +} diff --git a/core/static/webpack/jquery-index.js b/core/static/webpack/jquery-index.js index 7c5159fe..569d26e8 100644 --- a/core/static/webpack/jquery-index.js +++ b/core/static/webpack/jquery-index.js @@ -13,9 +13,6 @@ require("jquery-ui/ui/widgets/tabs.js"); require("jquery-ui/themes/base/all.css"); -// We ship select2 here, otherwise it will duplicate jquery everywhere we load it -import "select2"; - /** * Simple wrapper to solve shorten not being able on legacy pages * @param {string} selector to be passed to jQuery diff --git a/core/static/webpack/utils/globals.ts b/core/static/webpack/utils/globals.ts index 905f9740..b4f9a457 100644 --- a/core/static/webpack/utils/globals.ts +++ b/core/static/webpack/utils/globals.ts @@ -3,6 +3,7 @@ import type { Alpine as AlpineType } from "alpinejs"; declare global { const Alpine: AlpineType; const gettext: (text: string) => string; + const interpolate: (fmt: string, args: string[] | T, isNamed?: boolean) => string; } /** diff --git a/core/static/webpack/utils/select2.ts b/core/static/webpack/utils/select2.ts deleted file mode 100644 index 44058ac3..00000000 --- a/core/static/webpack/utils/select2.ts +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Builders to use Select2 in our templates. - * - * This comes with two flavours : local data or remote data. - * - * # Local data source - * - * To use local data source, you must define an array - * in your JS code, having the fields `id` and `text`. - * - * ```js - * const data = [ - * {id: 1, text: "foo"}, - * {id: 2, text: "bar"}, - * ]; - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: localDataSource(data) - * })); - * ``` - * - * You can also define a callback that return ids to exclude : - * - * ```js - * const data = [ - * {id: 1, text: "foo"}, - * {id: 2, text: "bar"}, - * {id: 3, text: "to exclude"}, - * ]; - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: localDataSource(data, { - * excluded: () => data.filter((i) => i.text === "to exclude").map((i) => parseInt(i)) - * }) - * })); - * ``` - * - * # Remote data source - * - * Select2 with remote data sources are similar to those with local - * data, but with some more parameters, like `resultConverter`, - * which takes a callback that must return a `Select2Object`. - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * excluded: () => [1, 2], // exclude users 1 and 2 from the search - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * }) - * })); - * ``` - * - * # Overrides - * - * Dealing with a select2 may be complex. - * That's why, when defining a select, - * you may add an override parameter, - * in which you can declare any parameter defined in the - * Select2 documentation. - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * overrides: { - * delay: 500 - * } - * }) - * })); - * ``` - * - * # Caveats with exclude - * - * With local data source, select2 evaluates the data only once. - * Thus, modify the exclude after the initialisation is a no-op. - * - * With remote data source, the exclude list will be evaluated - * after each api response. - * It makes it possible to bind the data returned by the callback - * to some reactive data, thus making the exclude list dynamic. - * - * # Images - * - * Sometimes, you would like to display an image besides - * the text on the select items. - * In this case, fill the `pictureGetter` option : - * - * ```js - * import { makeUrl } from "#core:utils/api"; - * import {userSearchUsers } from "#openapi" - * document.addEventListener("DOMContentLoaded", () => sithSelect2({ - * element: document.getElementById("select2-input"), - * dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - * resultConverter: (user: AjaxResponse) => {id: user.id, text: (user.firstName as UserType)} - * }) - * pictureGetter: (user) => user.profilePict, - * })); - * ``` - * - * # Binding with alpine - * - * You can declare your select2 component in an Alpine data. - * - * ```html - * - *
- * - *

- *

- *
- * - * - * - */ - -import type { - AjaxOptions, - DataFormat, - GroupedDataFormat, - LoadingData, - Options, - PlainObject, -} from "select2"; -import "select2/dist/css/select2.css"; - -export interface Select2Object { - id: number; - text: string; -} - -// biome-ignore lint/suspicious/noExplicitAny: You have to do it at some point -export type RemoteResult = any; -export type AjaxResponse = AjaxOptions; - -interface DataSource { - ajax?: AjaxResponse | undefined; - data?: RemoteResult | DataFormat[] | GroupedDataFormat[] | undefined; -} - -interface Select2Options { - element: Element; - /** the data source, built with `localDataSource` or `remoteDataSource` */ - dataSource: DataSource; - excluded?: number[]; - /** A callback to get the picture field from the API response */ - pictureGetter?: (element: LoadingData | DataFormat | GroupedDataFormat) => string; - /** Any other select2 parameter to apply on the config */ - overrides?: Options; -} - -/** - * Create a new select2 with sith presets - */ -export function sithSelect2(options: Select2Options) { - const elem: PlainObject = $(options.element); - return elem.select2({ - theme: elem[0].multiple ? "classic" : "default", - minimumInputLength: 2, - templateResult: selectItemBuilder(options.pictureGetter), - ...options.dataSource, - ...(options.overrides ?? {}), - }); -} - -interface LocalSourceOptions { - excluded: () => number[]; -} - -/** - * Build a data source for a Select2 from a local array - */ -export function localDataSource( - source: Select2Object[] /** Array containing the data */, - options: LocalSourceOptions, -): DataSource { - if (options.excluded) { - const ids = options.excluded(); - return { data: source.filter((i) => !ids.includes(i.id)) }; - } - return { data: source }; -} - -interface RemoteSourceOptions { - /** A callback to the ids to exclude from the search */ - excluded?: () => number[]; - /** A converter for a value coming from the remote api */ - resultConverter?: ((obj: RemoteResult) => DataFormat | GroupedDataFormat) | undefined; - /** Any other select2 parameter to apply on the config */ - overrides?: AjaxOptions; -} - -/** - * Build a data source for a Select2 from a remote url - */ -export function remoteDataSource( - source: string /** url of the endpoint */, - options: RemoteSourceOptions, -): DataSource { - $.ajaxSetup({ - traditional: true, - }); - const params: AjaxOptions = { - url: source, - dataType: "json", - cache: true, - delay: 250, - data: function (params) { - return { - search: params.term, - exclude: [ - ...(this.val() || []).map((i: string) => Number.parseInt(i)), - ...(options.excluded ? options.excluded() : []), - ], - }; - }, - }; - if (options.resultConverter) { - params.processResults = (data) => ({ - results: data.results.map(options.resultConverter), - }); - } - if (options.overrides) { - Object.assign(params, options.overrides); - } - return { ajax: params }; -} - -export function itemFormatter(user: { loading: boolean; text: string }) { - if (user.loading) { - return user.text; - } -} - -/** - * Build a function to display the results - */ -export function selectItemBuilder(pictureGetter?: (item: RemoteResult) => string) { - return (item: RemoteResult) => { - const picture = typeof pictureGetter === "function" ? pictureGetter(item) : null; - const imgHtml = picture - ? `${item.text}` - : ""; - - return $(`
- ${imgHtml} - ${item.text} -
`); - }; -} diff --git a/core/static/webpack/utils/web-components.ts b/core/static/webpack/utils/web-components.ts new file mode 100644 index 00000000..2899a5af --- /dev/null +++ b/core/static/webpack/utils/web-components.ts @@ -0,0 +1,50 @@ +/** + * Class decorator to register components easily + * It's a wrapper around window.customElements.define + * What's nice about it is that you don't separate the component registration + * and the class definition + **/ +export function registerComponent(name: string, options?: ElementDefinitionOptions) { + return (component: CustomElementConstructor) => { + window.customElements.define(name, component, options); + }; +} + +/** + * Safari doesn't support inheriting from HTML tags on web components + * The technique is to: + * create a new web component + * create the desired type inside + * pass all attributes to the child component + * store is at as `node` inside the parent + * + * Since we can't use the generic type to instantiate the node, we create a generator function + * + * ```js + * class MyClass extends inheritHtmlElement("select") { + * // do whatever + * } + * ``` + **/ +export function inheritHtmlElement(tagName: K) { + return class Inherited extends HTMLElement { + protected node: HTMLElementTagNameMap[K]; + + constructor() { + super(); + this.node = document.createElement(tagName); + const attributes: Attr[] = []; // We need to make a copy to delete while iterating + for (const attr of this.attributes) { + if (attr.name in this.node) { + attributes.push(attr); + } + } + + for (const attr of attributes) { + this.removeAttributeNode(attr); + this.node.setAttributeNode(attr); + } + this.appendChild(this.node); + } + }; +} diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 76c6392b..8ce2eb80 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -5,6 +5,7 @@ {% block title %}{% trans %}Welcome!{% endtrans %}{% endblock %} - Association des Étudiants UTBM + diff --git a/core/templates/core/widgets/markdown_textarea.jinja b/core/templates/core/widgets/markdown_textarea.jinja index 2412497d..287e4521 100644 --- a/core/templates/core/widgets/markdown_textarea.jinja +++ b/core/templates/core/widgets/markdown_textarea.jinja @@ -1,13 +1,7 @@
- + {% if widget.value %}{{ widget.value }}{% endif %} {# The easymde script can be included twice, it's safe in the code #} -
diff --git a/core/templatetags/renderer.py b/core/templatetags/renderer.py index f2d677d3..383f0383 100644 --- a/core/templatetags/renderer.py +++ b/core/templatetags/renderer.py @@ -50,7 +50,7 @@ def phonenumber( try: parsed = phonenumbers.parse(value, country) return phonenumbers.format_number(parsed, number_format) - except phonenumbers.NumberParseException as e: + except phonenumbers.NumberParseException: return value diff --git a/core/tests/test_core.py b/core/tests/test_core.py index 2604901a..b803cefa 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -343,7 +343,7 @@ class TestUserTools: response = client.get(reverse("core:user_tools")) assertRedirects( response, - expected_url=f"/login?next=%2Fuser%2Ftools%2F", + expected_url="/login?next=%2Fuser%2Ftools%2F", target_status_code=301, ) diff --git a/core/tests/test_family.py b/core/tests/test_family.py index 58d90a92..795de590 100644 --- a/core/tests/test_family.py +++ b/core/tests/test_family.py @@ -73,7 +73,7 @@ class TestFetchFamilyApi(TestCase): self.client.force_login(self.main_user) response = self.client.get( reverse("api:family_graph", args=[self.main_user.id]) - + f"?godfathers_depth=0&godchildren_depth=0" + + "?godfathers_depth=0&godchildren_depth=0" ) assert response.status_code == 200 assert [u["id"] for u in response.json()["users"]] == [self.main_user.id] @@ -91,7 +91,7 @@ class TestFetchFamilyApi(TestCase): self.client.force_login(self.main_user) response = self.client.get( reverse("api:family_graph", args=[self.main_user.id]) - + f"?godfathers_depth=10&godchildren_depth=10" + + "?godfathers_depth=10&godchildren_depth=10" ) assert response.status_code == 200 assert [u["id"] for u in response.json()["users"]] == [ @@ -126,7 +126,7 @@ class TestFetchFamilyApi(TestCase): self.client.force_login(self.main_user) response = self.client.get( reverse("api:family_graph", args=[self.main_user.id]) - + f"?godfathers_depth=1&godchildren_depth=1" + + "?godfathers_depth=1&godchildren_depth=1" ) assert response.status_code == 200 assert [u["id"] for u in response.json()["users"]] == [ @@ -150,7 +150,7 @@ class TestFetchFamilyApi(TestCase): self.client.force_login(self.main_user) response = self.client.get( reverse("api:family_graph", args=[self.main_user.id]) - + f"?godfathers_depth=10&godchildren_depth=0" + + "?godfathers_depth=10&godchildren_depth=0" ) assert response.status_code == 200 assert [u["id"] for u in response.json()["users"]] == [ diff --git a/core/urls.py b/core/urls.py index ac282a37..a3b4f7d8 100644 --- a/core/urls.py +++ b/core/urls.py @@ -29,13 +29,67 @@ from core.converters import ( FourDigitYearConverter, TwoDigitMonthConverter, ) -from core.views import * +from core.views import ( + FileDeleteView, + FileEditPropView, + FileEditView, + FileListView, + FileModerateView, + FileModerationView, + FileView, + GiftCreateView, + GiftDeleteView, + GroupCreateView, + GroupDeleteView, + GroupEditView, + GroupListView, + GroupTemplateView, + NotificationList, + PageCreateView, + PageDeleteView, + PageEditView, + PageHistView, + PageListView, + PagePropView, + PageRevView, + PageView, + SithLoginView, + SithPasswordChangeDoneView, + SithPasswordChangeView, + SithPasswordResetCompleteView, + SithPasswordResetConfirmView, + SithPasswordResetDoneView, + SithPasswordResetView, + UserAccountDetailView, + UserAccountView, + UserClubView, + UserCreationView, + UserGodfathersTreeView, + UserGodfathersView, + UserListView, + UserMiniView, + UserPicturesView, + UserPreferencesView, + UserStatsView, + UserToolsView, + UserUpdateGroupView, + UserUpdateProfileView, + UserView, + delete_user_godfather, + index, + logout, + notification, + password_root_change, + search_json, + search_user_json, + search_view, + send_file, +) register_converter(FourDigitYearConverter, "yyyy") register_converter(TwoDigitMonthConverter, "mm") register_converter(BooleanStringConverter, "bool") - urlpatterns = [ path("", index, name="index"), path("notifications/", NotificationList.as_view(), name="notification_list"), @@ -80,27 +134,17 @@ urlpatterns = [ path("group/new/", GroupCreateView.as_view(), name="group_new"), path("group//", GroupEditView.as_view(), name="group_edit"), path( - "group//delete/", - GroupDeleteView.as_view(), - name="group_delete", + "group//delete/", GroupDeleteView.as_view(), name="group_delete" ), path( - "group//detail/", - GroupTemplateView.as_view(), - name="group_detail", + "group//detail/", GroupTemplateView.as_view(), name="group_detail" ), # User views path("user/", UserListView.as_view(), name="user_list"), - path( - "user//mini/", - UserMiniView.as_view(), - name="user_profile_mini", - ), + path("user//mini/", UserMiniView.as_view(), name="user_profile_mini"), path("user//", UserView.as_view(), name="user_profile"), path( - "user//pictures/", - UserPicturesView.as_view(), - name="user_pictures", + "user//pictures/", UserPicturesView.as_view(), name="user_pictures" ), path( "user//godfathers/", @@ -117,28 +161,14 @@ urlpatterns = [ delete_user_godfather, name="user_godfathers_delete", ), - path( - "user//edit/", - UserUpdateProfileView.as_view(), - name="user_edit", - ), + path("user//edit/", UserUpdateProfileView.as_view(), name="user_edit"), path("user//clubs/", UserClubView.as_view(), name="user_clubs"), + path("user//prefs/", UserPreferencesView.as_view(), name="user_prefs"), path( - "user//prefs/", - UserPreferencesView.as_view(), - name="user_prefs", - ), - path( - "user//groups/", - UserUpdateGroupView.as_view(), - name="user_groups", + "user//groups/", UserUpdateGroupView.as_view(), name="user_groups" ), path("user/tools/", UserToolsView.as_view(), name="user_tools"), - path( - "user//account/", - UserAccountView.as_view(), - name="user_account", - ), + path("user//account/", UserAccountView.as_view(), name="user_account"), path( "user//account///", UserAccountDetailView.as_view(), @@ -179,42 +209,18 @@ urlpatterns = [ ), path("file/moderation/", FileModerationView.as_view(), name="file_moderation"), path( - "file//moderate/", - FileModerateView.as_view(), - name="file_moderate", + "file//moderate/", FileModerateView.as_view(), name="file_moderate" ), path("file//download/", send_file, name="download"), # Page views path("page/", PageListView.as_view(), name="page_list"), path("page/create/", PageCreateView.as_view(), name="page_new"), + path("page//delete/", PageDeleteView.as_view(), name="page_delete"), + path("page//edit/", PageEditView.as_view(), name="page_edit"), + path("page//prop/", PagePropView.as_view(), name="page_prop"), + path("page//hist/", PageHistView.as_view(), name="page_hist"), path( - "page//delete/", - PageDeleteView.as_view(), - name="page_delete", - ), - path( - "page//edit/", - PageEditView.as_view(), - name="page_edit", - ), - path( - "page//prop/", - PagePropView.as_view(), - name="page_prop", - ), - path( - "page//hist/", - PageHistView.as_view(), - name="page_hist", - ), - path( - "page//rev//", - PageRevView.as_view(), - name="page_rev", - ), - path( - "page//", - PageView.as_view(), - name="page", + "page//rev//", PageRevView.as_view(), name="page_rev" ), + path("page//", PageView.as_view(), name="page"), ] diff --git a/core/utils.py b/core/utils.py index df69f604..5b6191f6 100644 --- a/core/utils.py +++ b/core/utils.py @@ -127,7 +127,7 @@ def resize_image_explicit( def exif_auto_rotate(image): - for orientation in ExifTags.TAGS.keys(): + for orientation in ExifTags.TAGS: if ExifTags.TAGS[orientation] == "Orientation": break exif = dict(image._getexif().items()) diff --git a/core/views/__init__.py b/core/views/__init__.py index a2426667..68a91065 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -25,6 +25,7 @@ import types from typing import Any +from django.conf import settings from django.contrib.auth.mixins import AccessMixin from django.core.exceptions import ( ImproperlyConfigured, @@ -35,6 +36,7 @@ from django.http import ( HttpResponseNotFound, HttpResponseServerError, ) +from django.shortcuts import render from django.utils.functional import cached_property from django.views.generic.base import View from django.views.generic.detail import SingleObjectMixin @@ -79,9 +81,7 @@ def can_edit_prop(obj: Any, user: User) -> bool: raise PermissionDenied ``` """ - if obj is None or user.is_owner(obj): - return True - return False + return obj is None or user.is_owner(obj) def can_edit(obj: Any, user: User) -> bool: @@ -232,7 +232,9 @@ class UserIsRootMixin(GenericContentPermissionMixinBuilder): PermissionDenied: if the user isn't root """ - permission_function = lambda obj, user: user.is_root + @staticmethod + def permission_function(obj: Any, user: User): + return user.is_root class FormerSubscriberMixin(AccessMixin): @@ -304,10 +306,10 @@ class QuickNotifMixin: kwargs["quick_notifs"] = [] for n in self.quick_notif_list: kwargs["quick_notifs"].append(settings.SITH_QUICK_NOTIF[n]) - for k, v in settings.SITH_QUICK_NOTIF.items(): - for gk in self.request.GET.keys(): - if k == gk: - kwargs["quick_notifs"].append(v) + for key, val in settings.SITH_QUICK_NOTIF.items(): + for gk in self.request.GET: + if key == gk: + kwargs["quick_notifs"].append(val) return kwargs @@ -324,8 +326,10 @@ class DetailFormView(SingleObjectMixin, FormView): return super().get_object() -from .files import * -from .group import * -from .page import * -from .site import * -from .user import * +# F403: those star-imports would be hellish to refactor +# E402: putting those import at the top of the file would also be difficult +from .files import * # noqa: F403 E402 +from .group import * # noqa: F403 E402 +from .page import * # noqa: F403 E402 +from .site import * # noqa: F403 E402 +from .user import * # noqa: F403 E402 diff --git a/core/views/files.py b/core/views/files.py index c94ea427..3df5c014 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -193,7 +193,7 @@ class FileEditView(CanEditMixin, UpdateView): def get_form_class(self): fields = ["name", "is_moderated"] if self.object.is_file: - fields = ["file"] + fields + fields = ["file", *fields] return modelform_factory(SithFile, fields=fields) def get_success_url(self): @@ -283,38 +283,38 @@ class FileView(CanViewMixin, DetailView, FormMixin): `obj` is the SithFile object you want to put in the clipboard, or where you want to paste the clipboard """ - if "delete" in request.POST.keys(): + if "delete" in request.POST: for f_id in request.POST.getlist("file_list"): - sf = SithFile.objects.filter(id=f_id).first() - if sf: - sf.delete() - if "clear" in request.POST.keys(): + file = SithFile.objects.filter(id=f_id).first() + if file: + file.delete() + if "clear" in request.POST: request.session["clipboard"] = [] - if "cut" in request.POST.keys(): - for f_id in request.POST.getlist("file_list"): - f_id = int(f_id) + if "cut" in request.POST: + for f_id_str in request.POST.getlist("file_list"): + f_id = int(f_id_str) if ( f_id in [c.id for c in obj.children.all()] and f_id not in request.session["clipboard"] ): request.session["clipboard"].append(f_id) - if "paste" in request.POST.keys(): + if "paste" in request.POST: for f_id in request.session["clipboard"]: - sf = SithFile.objects.filter(id=f_id).first() - if sf: - sf.move_to(obj) + file = SithFile.objects.filter(id=f_id).first() + if file: + file.move_to(obj) request.session["clipboard"] = [] request.session.modified = True def get(self, request, *args, **kwargs): self.form = self.get_form() - if "clipboard" not in request.session.keys(): + if "clipboard" not in request.session: request.session["clipboard"] = [] return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() - if "clipboard" not in request.session.keys(): + if "clipboard" not in request.session: request.session["clipboard"] = [] if request.user.can_edit(self.object): # XXX this call can fail! @@ -398,6 +398,6 @@ class FileModerateView(CanEditPropMixin, SingleObjectMixin): self.object.is_moderated = True self.object.moderator = request.user self.object.save() - if "next" in self.request.GET.keys(): + if "next" in self.request.GET: return redirect(self.request.GET["next"]) return redirect("core:file_moderation") diff --git a/core/views/forms.py b/core/views/forms.py index 29de3ab4..232c938b 100644 --- a/core/views/forms.py +++ b/core/views/forms.py @@ -29,6 +29,7 @@ from captcha.fields import CaptchaField from django import forms from django.conf import settings from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.contrib.staticfiles.storage import staticfiles_storage from django.core.exceptions import ValidationError from django.db import transaction from django.forms import ( @@ -38,7 +39,6 @@ from django.forms import ( Textarea, TextInput, ) -from django.templatetags.static import static from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from phonenumber_field.widgets import RegionalPhoneNumberWidget @@ -72,8 +72,8 @@ class MarkdownInput(Textarea): context = super().get_context(name, value, attrs) context["statics"] = { - "js": static("webpack/easymde-index.js"), - "css": static("webpack/easymde-index.css"), + "js": staticfiles_storage.url("webpack/easymde-index.ts"), + "css": staticfiles_storage.url("webpack/easymde-index.css"), } return context @@ -140,7 +140,7 @@ class SelectUser(TextInput): class LoginForm(AuthenticationForm): def __init__(self, *arg, **kwargs): - if "data" in kwargs.keys(): + if "data" in kwargs: from counter.models import Customer data = kwargs["data"].copy() @@ -157,7 +157,7 @@ class LoginForm(AuthenticationForm): else: user = User.objects.filter(username=data["username"]).first() data["username"] = user.username - except: + except: # noqa E722 I don't know what error is supposed to be raised here pass kwargs["data"] = data super().__init__(*arg, **kwargs) diff --git a/core/views/page.py b/core/views/page.py index 08c8fad9..01fd59f6 100644 --- a/core/views/page.py +++ b/core/views/page.py @@ -55,7 +55,7 @@ class PageView(CanViewMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if "page" not in context.keys(): + if "page" not in context: context["new_page"] = self.kwargs["page_name"] return context @@ -92,22 +92,16 @@ class PageRevView(CanViewMixin, DetailView): ) return res - def get_object(self): + def get_object(self, *args, **kwargs): self.page = Page.get_page_by_full_name(self.kwargs["page_name"]) return self.page def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if self.page is not None: - context["page"] = self.page - try: - rev = self.page.revisions.get(id=self.kwargs["rev"]) - context["rev"] = rev - except: - # By passing, the template will just display the normal page without taking revision into account - pass - else: - context["new_page"] = self.kwargs["page_name"] + if not self.page: + return context | {"new_page": self.kwargs["page_name"]} + context["page"] = self.page + context["rev"] = self.page.revisions.filter(id=self.kwargs["rev"]).first() return context @@ -118,7 +112,7 @@ class PageCreateView(CanCreateMixin, CreateView): def get_initial(self): init = {} - if "page" in self.request.GET.keys(): + if "page" in self.request.GET: page_name = self.request.GET["page"] parent_name = "/".join(page_name.split("/")[:-1]) parent = Page.get_page_by_full_name(parent_name) @@ -145,18 +139,8 @@ class PagePropView(CanEditPagePropMixin, UpdateView): slug_field = "_full_name" slug_url_kwarg = "page_name" - def get_object(self): - o = super().get_object() - # Create the page if it does not exists - # if p == None: - # parent_name = '/'.join(page_name.split('/')[:-1]) - # name = page_name.split('/')[-1] - # if parent_name == "": - # p = Page(name=name) - # else: - # parent = Page.get_page_by_full_name(parent_name) - # p = Page(name=name, parent=parent) - self.page = o + def get_object(self, queryset=None): + self.page = super().get_object() try: self.page.set_lock_recursive(self.request.user) except LockError as e: diff --git a/core/views/site.py b/core/views/site.py index 37300414..06e2a8e3 100644 --- a/core/views/site.py +++ b/core/views/site.py @@ -53,11 +53,8 @@ class NotificationList(ListView): if self.request.user.is_anonymous: return Notification.objects.none() # TODO: Bulk update in django 2.2 - if "see_all" in self.request.GET.keys(): - for n in self.request.user.notifications.filter(viewed=False): - n.viewed = True - n.save() - + if "see_all" in self.request.GET: + self.request.user.notifications.filter(viewed=False).update(viewed=True) return self.request.user.notifications.order_by("-date")[:20] diff --git a/core/views/user.py b/core/views/user.py index 027a7edc..98ebcce6 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -255,8 +255,10 @@ class UserTabsMixin(TabedViewMixin): "name": _("Groups"), } ) - try: - if user.customer and ( + if ( + hasattr(user, "customer") + and user.customer + and ( user == self.request.user or self.request.user.is_in_group( pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID @@ -266,25 +268,22 @@ class UserTabsMixin(TabedViewMixin): + settings.SITH_BOARD_SUFFIX ) or self.request.user.is_root - ): - tab_list.append( - { - "url": reverse("core:user_stats", kwargs={"user_id": user.id}), - "slug": "stats", - "name": _("Stats"), - } - ) - tab_list.append( - { - "url": reverse( - "core:user_account", kwargs={"user_id": user.id} - ), - "slug": "account", - "name": _("Account") + " (%s €)" % user.customer.amount, - } - ) - except: - pass + ) + ): + tab_list.append( + { + "url": reverse("core:user_stats", kwargs={"user_id": user.id}), + "slug": "stats", + "name": _("Stats"), + } + ) + tab_list.append( + { + "url": reverse("core:user_account", kwargs={"user_id": user.id}), + "slug": "account", + "name": _("Account") + " (%s €)" % user.customer.amount, + } + ) return tab_list diff --git a/counter/admin.py b/counter/admin.py index 42943338..09f0e273 100644 --- a/counter/admin.py +++ b/counter/admin.py @@ -15,7 +15,19 @@ from django.contrib import admin from haystack.admin import SearchModelAdmin -from counter.models import * +from counter.models import ( + AccountDump, + BillingInfo, + CashRegisterSummary, + Counter, + Customer, + Eticket, + Permanency, + Product, + ProductType, + Refilling, + Selling, +) @admin.register(Product) diff --git a/counter/management/commands/dump_warning_mail.py b/counter/management/commands/dump_warning_mail.py index 2b8fbfdd..9b966494 100644 --- a/counter/management/commands/dump_warning_mail.py +++ b/counter/management/commands/dump_warning_mail.py @@ -1,4 +1,5 @@ import logging +import time from smtplib import SMTPException from django.conf import settings @@ -25,9 +26,34 @@ class Command(BaseCommand): self.logger.setLevel(logging.INFO) super().__init__(*args, **kwargs) + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Do not send the mails, just display the number of users concerned", + ) + parser.add_argument( + "-d", + "--delay", + type=float, + default=0, + help="Delay in seconds between each mail sent", + ) + def handle(self, *args, **options): - users = list(self._get_users()) + users: list[User] = list(self._get_users()) self.stdout.write(f"{len(users)} users will be warned of their account dump") + if options["verbosity"] > 1: + self.stdout.write("Users concerned:\n") + self.stdout.write( + "\n".join( + f" - {user.get_display_name()} ({user.email}) : " + f"{user.customer.amount} €" + for user in users + ) + ) + if options["dry_run"]: + return dumps = [] for user in users: is_success = self._send_mail(user) @@ -38,6 +64,10 @@ class Command(BaseCommand): warning_mail_error=not is_success, ) ) + if options["delay"]: + # avoid spamming the mail server too much + time.sleep(options["delay"]) + AccountDump.objects.bulk_create(dumps) self.stdout.write("Finished !") diff --git a/counter/models.py b/counter/models.py index e6d5b061..1f1c6b51 100644 --- a/counter/models.py +++ b/counter/models.py @@ -154,7 +154,7 @@ class Customer(models.Model): self.save() def get_full_url(self): - return "".join(["https://", settings.SITH_URL, self.get_absolute_url()]) + return f"https://{settings.SITH_URL}{self.get_absolute_url()}" class BillingInfo(models.Model): @@ -287,9 +287,7 @@ class ProductType(models.Model): """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False - if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): - return True - return False + return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) class Product(models.Model): @@ -346,21 +344,19 @@ class Product(models.Model): @property def is_record_product(self): - return settings.SITH_ECOCUP_CONS == self.id + return self.id == settings.SITH_ECOCUP_CONS @property def is_unrecord_product(self): - return settings.SITH_ECOCUP_DECO == self.id + return self.id == settings.SITH_ECOCUP_DECO def is_owned_by(self, user): """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False - if user.is_in_group( + return user.is_in_group( pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID - ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): - return True - return False + ) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) def can_be_sold_to(self, user: User) -> bool: """Check if whether the user given in parameter has the right to buy @@ -392,10 +388,7 @@ class Product(models.Model): buying_groups = list(self.buying_groups.all()) if not buying_groups: return True - for group in buying_groups: - if user.is_in_group(pk=group.id): - return True - return False + return any(user.is_in_group(pk=group.id) for group in buying_groups) @property def profit(self): @@ -887,27 +880,19 @@ class Selling(models.Model): "You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s." ) % { "event": event, - "url": "".join( - ( - '', - self.customer.get_full_url(), - "", - ) + "url": ( + f'' + f"{self.customer.get_full_url()}" ), - "eticket": "".join( - ( - '', - self.get_eticket_full_url(), - "", - ) + "eticket": ( + f'' + f"{self.get_eticket_full_url()}" ), } message_txt = _( - "You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s." + "You bought an eticket for the event %(event)s.\n" + "You can download it directly from this link %(eticket)s.\n" + "You can also retrieve all your e-tickets on your account page %(url)s." ) % { "event": event, "url": self.customer.get_full_url(), @@ -919,7 +904,7 @@ class Selling(models.Model): def get_eticket_full_url(self): eticket_url = reverse("counter:eticket_pdf", kwargs={"selling_id": self.id}) - return "".join(["https://", settings.SITH_URL, eticket_url]) + return f"https://{settings.SITH_URL}{eticket_url}" class Permanency(models.Model): @@ -1019,15 +1004,15 @@ class CashRegisterSummary(models.Model): elif name == "hundred_euros": return self.items.filter(value=100, is_check=False).first() elif name == "check_1": - return checks[0] if 0 < len(checks) else None + return checks[0] if len(checks) > 0 else None elif name == "check_2": - return checks[1] if 1 < len(checks) else None + return checks[1] if len(checks) > 1 else None elif name == "check_3": - return checks[2] if 2 < len(checks) else None + return checks[2] if len(checks) > 2 else None elif name == "check_4": - return checks[3] if 3 < len(checks) else None + return checks[3] if len(checks) > 3 else None elif name == "check_5": - return checks[4] if 4 < len(checks) else None + return checks[4] if len(checks) > 4 else None else: return object.__getattribute__(self, name) @@ -1035,9 +1020,7 @@ class CashRegisterSummary(models.Model): """Method to see if that object can be edited by the given user.""" if user.is_anonymous: return False - if user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID): - return True - return False + return user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID) def get_total(self): t = 0 diff --git a/counter/signals.py b/counter/signals.py index a002cdec..27d7c142 100644 --- a/counter/signals.py +++ b/counter/signals.py @@ -51,7 +51,7 @@ def write_log(instance, operation_type): # Return None by default return None - log = OperationLog( + OperationLog( label=str(instance), operator=get_user(), operation_type=operation_type, diff --git a/counter/templates/counter/account_dump_warning_mail.jinja b/counter/templates/counter/account_dump_warning_mail.jinja index 1d7dc8ed..b1374fc0 100644 --- a/counter/templates/counter/account_dump_warning_mail.jinja +++ b/counter/templates/counter/account_dump_warning_mail.jinja @@ -1,43 +1,40 @@ -

- Bonjour, -

+{% trans %}Hello{% endtrans %}, -

- {%- trans date=last_subscription_date|date(DATETIME_FORMAT) -%} - You received this email because your last subscription to the - Students' association ended on {{ date }}. - {%- endtrans -%} -

+{% trans trimmed date=last_subscription_date|date(DATETIME_FORMAT) -%} + You received this email because your last subscription to the + Students' association ended on {{ date }}. +{%- endtrans %} -

- {%- trans date=dump_date|date(DATETIME_FORMAT), amount=balance -%} - In accordance with the Internal Regulations, the balance of any - inactive AE account for more than 2 years automatically goes back - to the AE. - The money present on your account will therefore be recovered in full - on {{ date }}, for a total of {{ amount }} €. - {%- endtrans -%} -

+{% trans trimmed date=dump_date|date(DATETIME_FORMAT), amount=balance -%} + In accordance with the Internal Regulations, the balance of any + inactive AE account for more than 2 years automatically goes back + to the AE. + The money present on your account will therefore be recovered in full + on {{ date }}, for a total of {{ amount }} €. +{%- endtrans %} -

- {%- trans -%}However, if your subscription is renewed by this date, - your right to keep the money in your AE account will be renewed.{%- endtrans -%} -

+{% trans trimmed -%} + However, if your subscription is renewed by this date, + your right to keep the money in your AE account will be renewed. +{%- endtrans %} -{% if balance >= 10 %} -

- {%- trans -%}You can also request a refund by sending an email to - ae@utbm.fr - before the aforementioned date.{%- endtrans -%} -

-{% endif %} +{% if balance >= 10 -%} + {% trans trimmed -%} + You can also request a refund by sending an email to ae@utbm.fr + before the aforementioned date. + {%- endtrans %} +{%- endif %} -

- {% trans %}Sincerely{% endtrans %}, -

+{% trans trimmed -%} + 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 later. + If you don't renew your subscription, there will be no consequences + other than the loss of the money currently in your AE account. +{%- endtrans %} -

- L'association des étudiants de l'UTBM
- 6, Boulevard Anatole France
- 90000 Belfort -

+{% trans %}Sincerely{% endtrans %}, + +L'association des étudiants de l'UTBM +6, Boulevard Anatole France +90000 Belfort diff --git a/counter/templates/counter/product_list.jinja b/counter/templates/counter/product_list.jinja index 959d6797..881d7800 100644 --- a/counter/templates/counter/product_list.jinja +++ b/counter/templates/counter/product_list.jinja @@ -13,7 +13,7 @@

{{ product_type or _("Uncategorized") }}

{%- else -%} diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 781c7e5a..f37a25fb 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -503,7 +503,7 @@ class TestBarmanConnection(TestCase): ) response = self.client.get(reverse("counter:activity", args=[self.counter.id])) - assert not '
  • S' Kia
  • ' in str(response.content) + assert '
  • S' Kia
  • ' not in str(response.content) @pytest.mark.django_db @@ -853,7 +853,7 @@ class TestCustomerAccountId(TestCase): number = account_id[:-1] assert created is True assert number == "12346" - assert 6 == len(account_id) + assert len(account_id) == 6 assert account_id[-1] in string.ascii_lowercase assert customer.amount == 0 diff --git a/counter/urls.py b/counter/urls.py index 4dd12517..a2732925 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -15,7 +15,40 @@ from django.urls import path -from counter.views import * +from counter.views import ( + ActiveProductListView, + ArchivedProductListView, + CashSummaryEditView, + CashSummaryListView, + CounterActivityView, + CounterCashSummaryView, + CounterClick, + CounterCreateView, + CounterDeleteView, + CounterEditPropView, + CounterEditView, + CounterLastOperationsView, + CounterListView, + CounterMain, + CounterRefillingListView, + CounterStatView, + EticketCreateView, + EticketEditView, + EticketListView, + EticketPDFView, + InvoiceCallView, + ProductCreateView, + ProductEditView, + ProductTypeCreateView, + ProductTypeEditView, + ProductTypeListView, + RefillingDeleteView, + SellingDeleteView, + StudentCardDeleteView, + StudentCardFormView, + counter_login, + counter_logout, +) urlpatterns = [ path("/", CounterMain.as_view(), name="details"), diff --git a/counter/views.py b/counter/views.py index 56546818..9483d335 100644 --- a/counter/views.py +++ b/counter/views.py @@ -17,7 +17,7 @@ import re from datetime import datetime, timedelta from datetime import timezone as tz from http import HTTPStatus -from operator import attrgetter +from operator import itemgetter from typing import TYPE_CHECKING from urllib.parse import parse_qs @@ -91,16 +91,10 @@ class CounterAdminMixin(View): edit_club = [] def _test_group(self, user): - for grp_id in self.edit_group: - if user.is_in_group(pk=grp_id): - return True - return False + return any(user.is_in_group(pk=grp_id) for grp_id in self.edit_group) def _test_club(self, user): - for c in self.edit_club: - if c.can_be_edited_by(user): - return True - return False + return any(c.can_be_edited_by(user) for c in self.edit_club) def dispatch(self, request, *args, **kwargs): if not ( @@ -181,7 +175,7 @@ class CounterMain( def post(self, request, *args, **kwargs): self.object = self.get_object() if self.object.type == "BAR" and not ( - "counter_token" in self.request.session.keys() + "counter_token" in self.request.session and self.request.session["counter_token"] == self.object.token ): # Check the token to avoid the bar to be stolen return HttpResponseRedirect( @@ -219,7 +213,7 @@ class CounterMain( kwargs["barmen"] = self.object.barmen_list elif self.request.user.is_authenticated: kwargs["barmen"] = [self.request.user] - if "last_basket" in self.request.session.keys(): + if "last_basket" in self.request.session: kwargs["last_basket"] = self.request.session.pop("last_basket") kwargs["last_customer"] = self.request.session.pop("last_customer") kwargs["last_total"] = self.request.session.pop("last_total") @@ -294,7 +288,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): def get(self, request, *args, **kwargs): """Simple get view.""" - if "basket" not in request.session.keys(): # Init the basket session entry + if "basket" not in request.session: # Init the basket session entry request.session["basket"] = {} request.session["basket_total"] = 0 request.session["not_enough"] = False # Reset every variable @@ -318,7 +312,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): ): # Check that at least one barman is logged in return self.cancel(request) if self.object.type == "BAR" and not ( - "counter_token" in self.request.session.keys() + "counter_token" in self.request.session and self.request.session["counter_token"] == self.object.token ): # Also check the token to avoid the bar to be stolen return HttpResponseRedirect( @@ -329,7 +323,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): ) + "?bad_location" ) - if "basket" not in request.session.keys(): + if "basket" not in request.session: request.session["basket"] = {} request.session["basket_total"] = 0 request.session["not_enough"] = False # Reset every variable @@ -386,13 +380,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): def get_total_quantity_for_pid(self, request, pid): pid = str(pid) - try: - return ( - request.session["basket"][pid]["qty"] - + request.session["basket"][pid]["bonus_qty"] - ) - except: + if pid not in request.session["basket"]: return 0 + return ( + request.session["basket"][pid]["qty"] + + request.session["basket"][pid]["bonus_qty"] + ) def compute_record_product(self, request, product=None): recorded = 0 @@ -808,7 +801,7 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): model = Product - queryset = Product.objects.annotate(type_name=F("product_type__name")) + queryset = Product.objects.values("id", "name", "code", "product_type__name") template_name = "counter/product_list.jinja" ordering = [ F("product_type__priority").desc(nulls_last=True), @@ -819,7 +812,7 @@ class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): def get_context_data(self, **kwargs): res = super().get_context_data(**kwargs) res["object_list"] = itertools.groupby( - res["object_list"], key=attrgetter("type_name") + res["object_list"], key=itemgetter("product_type__name") ) return res diff --git a/docs/howto/translation.md b/docs/howto/translation.md index 88366492..6ae299ed 100644 --- a/docs/howto/translation.md +++ b/docs/howto/translation.md @@ -24,6 +24,12 @@ Si le mot apparaît dans le template Jinja : {% trans %}Hello{% endtrans %} ``` +Si on est dans un fichier javascript ou typescript : + +```js +gettext("Hello"); +``` + ## Générer le fichier django.po La traduction se fait en trois étapes. @@ -32,7 +38,7 @@ l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serve ```bash ./manage.py makemessages --locale=fr -e py,jinja --ignore=node_modules # Pour le backend -./manage.py makemessages --locale=fr -d djangojs --ignore=node_modules # Pour le frontend +./manage.py makemessages --locale=fr -d djangojs -e js,ts --ignore=node_modules # Pour le frontend ``` ## Éditer le fichier django.po diff --git a/eboutic/admin.py b/eboutic/admin.py index 9786c522..b9be2774 100644 --- a/eboutic/admin.py +++ b/eboutic/admin.py @@ -13,8 +13,9 @@ # # from django.contrib import admin +from django.db.models import F, Sum -from eboutic.models import * +from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem @admin.register(Basket) diff --git a/eboutic/forms.py b/eboutic/forms.py index 84e4ad21..e15714cf 100644 --- a/eboutic/forms.py +++ b/eboutic/forms.py @@ -117,9 +117,7 @@ class BasketForm: """ if not self.error_messages and not self.correct_items: self.clean() - if self.error_messages: - return False - return True + return not self.error_messages @cached_property def errors(self) -> list[str]: diff --git a/eboutic/schemas.py b/eboutic/schemas.py index a8766f7e..6972dbd3 100644 --- a/eboutic/schemas.py +++ b/eboutic/schemas.py @@ -2,8 +2,6 @@ from typing import Annotated from ninja import ModelSchema, Schema from pydantic import Field, NonNegativeInt, PositiveInt, TypeAdapter - -# from phonenumber_field.phonenumber import PhoneNumber from pydantic_extra_types.phone_numbers import PhoneNumber, PhoneNumberValidator from counter.models import BillingInfo diff --git a/eboutic/tests/test_crypto.py b/eboutic/tests/test_crypto.py index ec0c88b6..9676ea99 100755 --- a/eboutic/tests/test_crypto.py +++ b/eboutic/tests/test_crypto.py @@ -7,11 +7,11 @@ import base64 from pathlib import Path +from typing import TYPE_CHECKING import pytest from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, @@ -19,6 +19,12 @@ from cryptography.hazmat.primitives.serialization import ( ) from django.conf import settings +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, + RSAPublicKey, + ) + def test_signature_valid(): """Test that data sent to the bank is correctly signed.""" diff --git a/eboutic/tests/tests.py b/eboutic/tests/tests.py index 31a77334..02a0cae5 100644 --- a/eboutic/tests/tests.py +++ b/eboutic/tests/tests.py @@ -24,9 +24,9 @@ import base64 import json import urllib +from typing import TYPE_CHECKING from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.serialization import load_pem_private_key from django.conf import settings @@ -38,6 +38,9 @@ from core.models import User from counter.models import Counter, Customer, Product, Selling from eboutic.models import Basket, BasketItem +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey + class TestEboutic(TestCase): @classmethod diff --git a/eboutic/urls.py b/eboutic/urls.py index 704cdd6b..968f814e 100644 --- a/eboutic/urls.py +++ b/eboutic/urls.py @@ -25,7 +25,14 @@ from django.urls import path, register_converter from eboutic.converters import PaymentResultConverter -from eboutic.views import * +from eboutic.views import ( + EbouticCommand, + EtransactionAutoAnswer, + e_transaction_data, + eboutic_main, + pay_with_sith, + payment_result, +) register_converter(PaymentResultConverter, "res") diff --git a/eboutic/views.py b/eboutic/views.py index 7e2776f0..14f129fd 100644 --- a/eboutic/views.py +++ b/eboutic/views.py @@ -17,11 +17,11 @@ import base64 import json from datetime import datetime from enum import Enum +from typing import TYPE_CHECKING import sentry_sdk from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 -from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.serialization import load_pem_public_key from django.conf import settings @@ -47,6 +47,9 @@ from eboutic.models import ( ) from eboutic.schemas import PurchaseItemList, PurchaseItemSchema +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey + @login_required @require_GET @@ -221,7 +224,7 @@ class EtransactionAutoAnswer(View): # Payment authorized: # * 'Error' is '00000' # * 'Auto' is in the request - if request.GET["Error"] == "00000" and "Auto" in request.GET.keys(): + if request.GET["Error"] == "00000" and "Auto" in request.GET: try: with transaction.atomic(): b = ( diff --git a/election/urls.py b/election/urls.py index 697b2464..0e0f8ae0 100644 --- a/election/urls.py +++ b/election/urls.py @@ -1,6 +1,22 @@ from django.urls import path -from election.views import * +from election.views import ( + CandidatureCreateView, + CandidatureDeleteView, + CandidatureUpdateView, + ElectionCreateView, + ElectionDeleteView, + ElectionDetailView, + ElectionListArchivedView, + ElectionListCreateView, + ElectionListDeleteView, + ElectionsListView, + ElectionUpdateView, + RoleCreateView, + RoleDeleteView, + RoleUpdateView, + VoteFormView, +) urlpatterns = [ path("", ElectionsListView.as_view(), name="list"), @@ -19,16 +35,10 @@ urlpatterns = [ name="delete_list", ), path( - "/role/create/", - RoleCreateView.as_view(), - name="create_role", + "/role/create/", RoleCreateView.as_view(), name="create_role" ), path("/role/edit/", RoleUpdateView.as_view(), name="update_role"), - path( - "/role/delete/", - RoleDeleteView.as_view(), - name="delete_role", - ), + path("/role/delete/", RoleDeleteView.as_view(), name="delete_role"), path( "/candidate/add/", CandidatureCreateView.as_view(), diff --git a/election/views.py b/election/views.py index 54ca37da..65aaf363 100644 --- a/election/views.py +++ b/election/views.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from ajax_select import make_ajax_field from ajax_select.fields import AutoCompleteSelectField from django import forms @@ -10,11 +12,14 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView -from core.models import User from core.views import CanCreateMixin, CanEditMixin, CanViewMixin from core.views.forms import MarkdownInput, SelectDateTime from election.models import Candidature, Election, ElectionList, Role, Vote +if TYPE_CHECKING: + from core.models import User + + # Custom form field @@ -23,7 +28,6 @@ class LimitedCheckboxField(forms.ModelMultipleChoiceField): def __init__(self, queryset, max_choice, **kwargs): self.max_choice = max_choice - widget = forms.CheckboxSelectMultiple() super().__init__(queryset, **kwargs) def clean(self, value): @@ -251,7 +255,7 @@ class VoteFormView(CanCreateMixin, FormView): def vote(self, election_data): with transaction.atomic(): - for role_title in election_data.keys(): + for role_title in election_data: # If we have a multiple choice field if isinstance(election_data[role_title], QuerySet): if election_data[role_title].count() > 0: @@ -444,28 +448,16 @@ class ElectionUpdateView(CanEditMixin, UpdateView): pk_url_kwarg = "election_id" def get_initial(self): - init = {} - try: - init["start_date"] = self.object.start_date.strftime("%Y-%m-%d %H:%M:%S") - except Exception: - pass - try: - init["end_date"] = self.object.end_date.strftime("%Y-%m-%d %H:%M:%S") - except Exception: - pass - try: - init["start_candidature"] = self.object.start_candidature.strftime( + return { + "start_date": self.object.start_date.strftime("%Y-%m-%d %H:%M:%S"), + "end_date": self.object.end_date.strftime("%Y-%m-%d %H:%M:%S"), + "start_candidature": self.object.start_candidature.strftime( "%Y-%m-%d %H:%M:%S" - ) - except Exception: - pass - try: - init["end_candidature"] = self.object.end_candidature.strftime( + ), + "end_candidature": self.object.end_candidature.strftime( "%Y-%m-%d %H:%M:%S" - ) - except Exception: - pass - return init + ), + } def get_success_url(self, **kwargs): return reverse_lazy("election:detail", kwargs={"election_id": self.object.id}) diff --git a/forum/admin.py b/forum/admin.py index 29fc54fd..7b578a94 100644 --- a/forum/admin.py +++ b/forum/admin.py @@ -16,7 +16,7 @@ from django.contrib import admin from haystack.admin import SearchModelAdmin -from forum.models import * +from forum.models import Forum, ForumMessage, ForumTopic @admin.register(Forum) diff --git a/forum/models.py b/forum/models.py index 3ac2fda3..85c487e9 100644 --- a/forum/models.py +++ b/forum/models.py @@ -25,6 +25,7 @@ from __future__ import annotations from datetime import datetime from datetime import timezone as tz from itertools import chain +from typing import Self from django.conf import settings from django.core.exceptions import ValidationError @@ -207,12 +208,12 @@ class Forum(models.Model): return self.get_parent_list() def get_parent_list(self): - l = [] - p = self.parent - while p is not None: - l.append(p) - p = p.parent - return l + parents = [] + current = self.parent + while current is not None: + parents.append(current) + current = current.parent + return parents @property def topic_number(self): @@ -228,12 +229,12 @@ class Forum(models.Model): def last_message(self): return self._last_message - def get_children_list(self): - l = [self.id] + def get_children_list(self) -> list[Self]: + children = [self.id] for c in self.children.all(): - l.append(c.id) - l += c.get_children_list() - return l + children.append(c.id) + children.extend(c.get_children_list()) + return children class ForumTopic(models.Model): diff --git a/forum/urls.py b/forum/urls.py index f006f6a9..70d50ad8 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -23,7 +23,26 @@ from django.urls import path -from forum.views import * +from forum.views import ( + ForumCreateView, + ForumDeleteView, + ForumDetailView, + ForumEditView, + ForumFavoriteTopics, + ForumLastUnread, + ForumMainView, + ForumMarkAllAsRead, + ForumMessageCreateView, + ForumMessageDeleteView, + ForumMessageEditView, + ForumMessageUndeleteView, + ForumMessageView, + ForumSearchView, + ForumTopicCreateView, + ForumTopicDetailView, + ForumTopicEditView, + ForumTopicSubscribeView, +) urlpatterns = [ path("", ForumMainView.as_view(), name="main"), @@ -35,21 +54,9 @@ urlpatterns = [ path("/", ForumDetailView.as_view(), name="view_forum"), path("/edit/", ForumEditView.as_view(), name="edit_forum"), path("/delete/", ForumDeleteView.as_view(), name="delete_forum"), - path( - "/new_topic/", - ForumTopicCreateView.as_view(), - name="new_topic", - ), - path( - "topic//", - ForumTopicDetailView.as_view(), - name="view_topic", - ), - path( - "topic//edit/", - ForumTopicEditView.as_view(), - name="edit_topic", - ), + path("/new_topic/", ForumTopicCreateView.as_view(), name="new_topic"), + path("topic//", ForumTopicDetailView.as_view(), name="view_topic"), + path("topic//edit/", ForumTopicEditView.as_view(), name="edit_topic"), path( "topic//new_message/", ForumMessageCreateView.as_view(), @@ -60,11 +67,7 @@ urlpatterns = [ ForumTopicSubscribeView.as_view(), name="toggle_subscribe_topic", ), - path( - "message//", - ForumMessageView.as_view(), - name="view_message", - ), + path("message//", ForumMessageView.as_view(), name="view_message"), path( "message//edit/", ForumMessageEditView.as_view(), diff --git a/galaxy/management/commands/generate_galaxy_test_data.py b/galaxy/management/commands/generate_galaxy_test_data.py index f1559e78..39334ddd 100644 --- a/galaxy/management/commands/generate_galaxy_test_data.py +++ b/galaxy/management/commands/generate_galaxy_test_data.py @@ -71,7 +71,7 @@ class Command(BaseCommand): def handle(self, *args, **options): self.logger = logging.getLogger("main") - if options["verbosity"] < 0 or 2 < options["verbosity"]: + if not 0 <= options["verbosity"] <= 2: warnings.warn( "verbosity level should be between 0 and 2 included", stacklevel=2 ) diff --git a/galaxy/management/commands/rule_galaxy.py b/galaxy/management/commands/rule_galaxy.py index 90510b3f..8589a741 100644 --- a/galaxy/management/commands/rule_galaxy.py +++ b/galaxy/management/commands/rule_galaxy.py @@ -40,7 +40,7 @@ class Command(BaseCommand): def handle(self, *args, **options): logger = logging.getLogger("main") - if options["verbosity"] < 0 or 2 < options["verbosity"]: + if not 0 <= options["verbosity"] <= 2: warnings.warn( "verbosity level should be between 0 and 2 included", stacklevel=2 ) diff --git a/galaxy/urls.py b/galaxy/urls.py index df8a957a..c697c52c 100644 --- a/galaxy/urls.py +++ b/galaxy/urls.py @@ -23,17 +23,9 @@ from django.urls import path -from galaxy.views import * +from galaxy.views import GalaxyDataView, GalaxyUserView urlpatterns = [ - path( - "/", - GalaxyUserView.as_view(), - name="user", - ), - path( - "data.json", - GalaxyDataView.as_view(), - name="data", - ), + path("/", GalaxyUserView.as_view(), name="user"), + path("data.json", GalaxyDataView.as_view(), name="data"), ] diff --git a/launderette/admin.py b/launderette/admin.py index 01d7e55b..c5d8bf07 100644 --- a/launderette/admin.py +++ b/launderette/admin.py @@ -14,7 +14,7 @@ # from django.contrib import admin -from launderette.models import * +from launderette.models import Launderette, Machine, Slot, Token @admin.register(Launderette) diff --git a/launderette/models.py b/launderette/models.py index 58458f14..5d6977e2 100644 --- a/launderette/models.py +++ b/launderette/models.py @@ -51,18 +51,14 @@ class Launderette(models.Model): unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() m = launderette_club.get_membership_for(user) - if m and m.role >= 9: - return True - return False + return bool(m and m.role >= 9) def can_be_edited_by(self, user): launderette_club = Club.objects.filter( unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() m = launderette_club.get_membership_for(user) - if m and m.role >= 2: - return True - return False + return bool(m and m.role >= 2) def can_be_viewed_by(self, user): return user.is_subscribed @@ -113,9 +109,7 @@ class Machine(models.Model): unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() m = launderette_club.get_membership_for(user) - if m and m.role >= 9: - return True - return False + return bool(m and m.role >= 9) class Token(models.Model): @@ -164,15 +158,7 @@ class Token(models.Model): unix_name=settings.SITH_LAUNDERETTE_MANAGER["unix_name"] ).first() m = launderette_club.get_membership_for(user) - if m and m.role >= 9: - return True - return False - - def is_avaliable(self): - if not self.borrow_date and not self.user: - return True - else: - return False + return bool(m and m.role >= 9) class Slot(models.Model): diff --git a/launderette/urls.py b/launderette/urls.py index c00663d1..3bcb7b99 100644 --- a/launderette/urls.py +++ b/launderette/urls.py @@ -15,22 +15,28 @@ from django.urls import path -from launderette.views import * +from launderette.views import ( + LaunderetteAdminView, + LaunderetteBookMainView, + LaunderetteBookView, + LaunderetteClickView, + LaunderetteCreateView, + LaunderetteEditView, + LaunderetteListView, + LaunderetteMainClickView, + LaunderetteMainView, + MachineCreateView, + MachineDeleteView, + MachineEditView, + SlotDeleteView, +) urlpatterns = [ # views path("", LaunderetteMainView.as_view(), name="launderette_main"), - path( - "slot//delete/", - SlotDeleteView.as_view(), - name="delete_slot", - ), + path("slot//delete/", SlotDeleteView.as_view(), name="delete_slot"), path("book/", LaunderetteBookMainView.as_view(), name="book_main"), - path( - "book//", - LaunderetteBookView.as_view(), - name="book_slot", - ), + path("book//", LaunderetteBookView.as_view(), name="book_slot"), path( "/click/", LaunderetteMainClickView.as_view(), diff --git a/launderette/views.py b/launderette/views.py index f79c1d15..efb001e0 100644 --- a/launderette/views.py +++ b/launderette/views.py @@ -19,7 +19,7 @@ from datetime import timezone as tz from django import forms from django.conf import settings -from django.db import DataError, transaction +from django.db import transaction from django.template import defaultfilters from django.urls import reverse_lazy from django.utils import dateparse, timezone @@ -73,15 +73,15 @@ class LaunderetteBookView(CanViewMixin, DetailView): self.machines = {} with transaction.atomic(): self.object = self.get_object() - if "slot_type" in request.POST.keys(): + if "slot_type" in request.POST: self.slot_type = request.POST["slot_type"] - if "slot" in request.POST.keys() and request.user.is_authenticated: + if "slot" in request.POST and request.user.is_authenticated: self.subscriber = request.user if self.subscriber.is_subscribed: self.date = dateparse.parse_datetime(request.POST["slot"]).replace( tzinfo=tz.utc ) - if self.slot_type == "WASHING": + if self.slot_type in ["WASHING", "DRYING"]: if self.check_slot(self.slot_type): Slot( user=self.subscriber, @@ -89,30 +89,21 @@ class LaunderetteBookView(CanViewMixin, DetailView): machine=self.machines[self.slot_type], type=self.slot_type, ).save() - elif self.slot_type == "DRYING": - if self.check_slot(self.slot_type): - Slot( - user=self.subscriber, - start_date=self.date, - machine=self.machines[self.slot_type], - type=self.slot_type, - ).save() - else: - if self.check_slot("WASHING") and self.check_slot( - "DRYING", self.date + timedelta(hours=1) - ): - Slot( - user=self.subscriber, - start_date=self.date, - machine=self.machines["WASHING"], - type="WASHING", - ).save() - Slot( - user=self.subscriber, - start_date=self.date + timedelta(hours=1), - machine=self.machines["DRYING"], - type="DRYING", - ).save() + elif self.check_slot("WASHING") and self.check_slot( + "DRYING", self.date + timedelta(hours=1) + ): + Slot( + user=self.subscriber, + start_date=self.date, + machine=self.machines["WASHING"], + type="WASHING", + ).save() + Slot( + user=self.subscriber, + start_date=self.date + timedelta(hours=1), + machine=self.machines["DRYING"], + type="DRYING", + ).save() return super().get(request, *args, **kwargs) def check_slot(self, machine_type, date=None): @@ -149,15 +140,17 @@ class LaunderetteBookView(CanViewMixin, DetailView): ): free = False if ( - self.slot_type == "BOTH" + ( + self.slot_type == "BOTH" + and self.check_slot("WASHING", h) + and self.check_slot("DRYING", h + timedelta(hours=1)) + ) + or self.slot_type == "WASHING" and self.check_slot("WASHING", h) - and self.check_slot("DRYING", h + timedelta(hours=1)) + or self.slot_type == "DRYING" + and self.check_slot("DRYING", h) ): free = True - elif self.slot_type == "WASHING" and self.check_slot("WASHING", h): - free = True - elif self.slot_type == "DRYING" and self.check_slot("DRYING", h): - free = True if free and datetime.now().replace(tzinfo=tz.utc) < h: kwargs["planning"][date].append(h) else: @@ -236,42 +229,39 @@ class ManageTokenForm(forms.Form): token_list = cleaned_data["tokens"].strip(" \n\r").split(" ") token_type = cleaned_data["token_type"] self.data = {} + + if cleaned_data["action"] not in ["BACK", "ADD", "DEL"]: + return + + tokens = list( + Token.objects.filter( + launderette=launderette, type=token_type, name__in=token_list + ) + ) + existing_names = {t.name for t in tokens} + if cleaned_data["action"] in ["BACK", "DEL"]: + for t in set(token_list) - existing_names: + self.add_error( + None, + _("Token %(token_name)s does not exists") % {"token_name": t}, + ) if cleaned_data["action"] == "BACK": - for t in token_list: - try: - tok = Token.objects.filter( - launderette=launderette, type=token_type, name=t - ).first() - tok.borrow_date = None - tok.user = None - tok.save() - except: - self.add_error( - None, - _("Token %(token_name)s does not exists") % {"token_name": t}, - ) - elif cleaned_data["action"] == "ADD": - for t in token_list: - try: - Token(launderette=launderette, type=token_type, name=t).save() - except DataError as e: - self.add_error(None, e) - except: - self.add_error( - None, - _("Token %(token_name)s already exists") % {"token_name": t}, - ) + Token.objects.filter(id__in=[t.id for t in tokens]).update( + borrow_date=None, user=None + ) elif cleaned_data["action"] == "DEL": + Token.objects.filter(id__in=[t.id for t in tokens]).delete() + elif cleaned_data["action"] == "ADD": + for name in existing_names: + self.add_error( + None, + _("Token %(token_name)s already exists") % {"token_name": name}, + ) for t in token_list: - try: - Token.objects.filter( - launderette=launderette, type=token_type, name=t - ).delete() - except: - self.add_error( - None, - _("Token %(token_name)s does not exists") % {"token_name": t}, - ) + if t == "": + self.add_error(None, _("Token name can not be blank")) + else: + Token(launderette=launderette, type=token_type, name=t).save() class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): @@ -288,13 +278,7 @@ class LaunderetteAdminView(CanEditPropMixin, BaseFormView, DetailView): def post(self, request, *args, **kwargs): self.object = self.get_object() - form = self.get_form() return super().post(request, *args, **kwargs) - form.launderette = self.object - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) def form_valid(self, form): """We handle here the redirection, passing the user id of the asked customer.""" @@ -353,7 +337,7 @@ class LaunderetteMainClickView(CanEditMixin, BaseFormView, DetailView): kwargs["counter"] = self.object.counter kwargs["form"] = self.get_form() kwargs["barmen"] = [self.request.user] - if "last_basket" in self.request.session.keys(): + if "last_basket" in self.request.session: kwargs["last_basket"] = self.request.session.pop("last_basket", None) kwargs["last_customer"] = self.request.session.pop("last_customer", None) kwargs["last_total"] = self.request.session.pop("last_total", None) @@ -479,7 +463,7 @@ class LaunderetteClickView(CanEditMixin, DetailView, BaseFormView): def get_context_data(self, **kwargs): """We handle here the login form for the barman.""" kwargs = super().get_context_data(**kwargs) - if "form" not in kwargs.keys(): + if "form" not in kwargs: kwargs["form"] = self.get_form() kwargs["counter"] = self.object.counter kwargs["customer"] = self.customer @@ -519,7 +503,7 @@ class MachineCreateView(CanCreateMixin, CreateView): def get_initial(self): ret = super().get_initial() - if "launderette" in self.request.GET.keys(): + if "launderette" in self.request.GET: obj = Launderette.objects.filter( id=int(self.request.GET["launderette"]) ).first() diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index f94df048..eed6620b 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-10-11 09:58+0200\n" +"POT-Creation-Date: 2024-10-20 12:42+0200\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -16,11 +16,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: accounting/models.py:62 accounting/models.py:103 accounting/models.py:136 -#: accounting/models.py:203 club/models.py:55 com/models.py:274 -#: com/models.py:293 counter/models.py:265 counter/models.py:298 -#: counter/models.py:456 forum/models.py:59 launderette/models.py:29 -#: launderette/models.py:84 launderette/models.py:122 +#: accounting/models.py:62 accounting/models.py:101 accounting/models.py:132 +#: accounting/models.py:190 club/models.py:55 com/models.py:274 +#: com/models.py:293 counter/models.py:265 counter/models.py:296 +#: counter/models.py:449 forum/models.py:60 launderette/models.py:29 +#: launderette/models.py:80 launderette/models.py:116 msgid "name" msgstr "nom" @@ -56,125 +56,125 @@ msgstr "site internet" msgid "company" msgstr "entreprise" -#: accounting/models.py:104 +#: accounting/models.py:102 msgid "iban" msgstr "IBAN" -#: accounting/models.py:105 +#: accounting/models.py:103 msgid "account number" msgstr "numéro de compte" -#: accounting/models.py:109 accounting/models.py:140 club/models.py:345 -#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:321 -#: counter/models.py:458 trombi/models.py:210 +#: accounting/models.py:107 accounting/models.py:136 club/models.py:345 +#: com/models.py:74 com/models.py:259 com/models.py:299 counter/models.py:319 +#: counter/models.py:451 trombi/models.py:209 msgid "club" msgstr "club" -#: accounting/models.py:114 +#: accounting/models.py:112 msgid "Bank account" msgstr "Compte en banque" -#: accounting/models.py:146 +#: accounting/models.py:142 msgid "bank account" msgstr "compte en banque" -#: accounting/models.py:151 +#: accounting/models.py:147 msgid "Club account" msgstr "Compte club" -#: accounting/models.py:192 +#: accounting/models.py:179 #, python-format msgid "%(club_account)s on %(bank_account)s" msgstr "%(club_account)s sur %(bank_account)s" -#: accounting/models.py:201 club/models.py:351 counter/models.py:944 -#: election/models.py:16 launderette/models.py:179 +#: accounting/models.py:188 club/models.py:351 counter/models.py:929 +#: election/models.py:16 launderette/models.py:165 msgid "start date" msgstr "date de début" -#: accounting/models.py:202 club/models.py:352 counter/models.py:945 +#: accounting/models.py:189 club/models.py:352 counter/models.py:930 #: election/models.py:17 msgid "end date" msgstr "date de fin" -#: accounting/models.py:204 +#: accounting/models.py:191 msgid "is closed" msgstr "est fermé" -#: accounting/models.py:209 accounting/models.py:519 +#: accounting/models.py:196 accounting/models.py:496 msgid "club account" msgstr "compte club" -#: accounting/models.py:212 accounting/models.py:272 counter/models.py:57 -#: counter/models.py:654 +#: accounting/models.py:199 accounting/models.py:255 counter/models.py:57 +#: counter/models.py:647 msgid "amount" msgstr "montant" -#: accounting/models.py:213 +#: accounting/models.py:200 msgid "effective_amount" msgstr "montant effectif" -#: accounting/models.py:216 +#: accounting/models.py:203 msgid "General journal" msgstr "Classeur" -#: accounting/models.py:264 +#: accounting/models.py:247 msgid "number" msgstr "numéro" -#: accounting/models.py:269 +#: accounting/models.py:252 msgid "journal" msgstr "classeur" -#: accounting/models.py:273 core/models.py:959 core/models.py:1479 -#: core/models.py:1524 core/models.py:1553 core/models.py:1577 -#: counter/models.py:664 counter/models.py:768 counter/models.py:980 -#: eboutic/models.py:57 eboutic/models.py:189 forum/models.py:311 -#: forum/models.py:412 +#: accounting/models.py:256 core/models.py:945 core/models.py:1456 +#: core/models.py:1501 core/models.py:1530 core/models.py:1554 +#: counter/models.py:657 counter/models.py:761 counter/models.py:965 +#: eboutic/models.py:57 eboutic/models.py:193 forum/models.py:312 +#: forum/models.py:413 msgid "date" msgstr "date" -#: accounting/models.py:274 counter/models.py:267 counter/models.py:981 -#: pedagogy/models.py:207 +#: accounting/models.py:257 counter/models.py:267 counter/models.py:966 +#: pedagogy/models.py:208 msgid "comment" msgstr "commentaire" -#: accounting/models.py:276 counter/models.py:666 counter/models.py:770 +#: accounting/models.py:259 counter/models.py:659 counter/models.py:763 #: subscription/models.py:56 msgid "payment method" msgstr "méthode de paiement" -#: accounting/models.py:281 +#: accounting/models.py:264 msgid "cheque number" msgstr "numéro de chèque" -#: accounting/models.py:286 eboutic/models.py:287 +#: accounting/models.py:269 eboutic/models.py:291 msgid "invoice" msgstr "facture" -#: accounting/models.py:291 +#: accounting/models.py:274 msgid "is done" msgstr "est fait" -#: accounting/models.py:295 +#: accounting/models.py:278 msgid "simple type" msgstr "type simplifié" -#: accounting/models.py:303 accounting/models.py:462 +#: accounting/models.py:286 accounting/models.py:441 msgid "accounting type" msgstr "type comptable" -#: accounting/models.py:311 accounting/models.py:450 accounting/models.py:483 -#: accounting/models.py:515 core/models.py:1552 core/models.py:1578 -#: counter/models.py:734 +#: accounting/models.py:294 accounting/models.py:429 accounting/models.py:460 +#: accounting/models.py:492 core/models.py:1529 core/models.py:1555 +#: counter/models.py:727 msgid "label" msgstr "étiquette" -#: accounting/models.py:317 +#: accounting/models.py:300 msgid "target type" msgstr "type de cible" -#: accounting/models.py:320 club/models.py:507 +#: accounting/models.py:303 club/models.py:505 #: club/templates/club/club_members.jinja:17 #: club/templates/club/club_old_members.jinja:8 #: club/templates/club/mailing.jinja:41 @@ -186,7 +186,7 @@ msgstr "type de cible" msgid "User" msgstr "Utilisateur" -#: accounting/models.py:321 club/models.py:410 +#: 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 @@ -210,35 +210,35 @@ msgstr "Utilisateur" msgid "Club" msgstr "Club" -#: accounting/models.py:322 core/views/user.py:281 +#: accounting/models.py:305 core/views/user.py:284 msgid "Account" msgstr "Compte" -#: accounting/models.py:323 +#: accounting/models.py:306 msgid "Company" msgstr "Entreprise" -#: accounting/models.py:324 core/models.py:337 sith/settings.py:420 +#: accounting/models.py:307 core/models.py:337 sith/settings.py:420 msgid "Other" msgstr "Autre" -#: accounting/models.py:327 +#: accounting/models.py:310 msgid "target id" msgstr "id de la cible" -#: accounting/models.py:329 +#: accounting/models.py:312 msgid "target label" msgstr "nom de la cible" -#: accounting/models.py:334 +#: accounting/models.py:317 msgid "linked operation" msgstr "opération liée" -#: accounting/models.py:366 +#: accounting/models.py:349 msgid "The date must be set." msgstr "La date doit être indiquée." -#: accounting/models.py:370 +#: accounting/models.py:353 #, python-format msgid "" "The date can not be before the start date of the journal, which is\n" @@ -247,16 +247,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:380 +#: accounting/models.py:363 msgid "Target does not exists" msgstr "La cible n'existe pas." -#: accounting/models.py:383 +#: accounting/models.py:366 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:388 +#: accounting/models.py:371 msgid "" "You need to provide ether a simplified accounting type or a standard " "accounting type" @@ -264,41 +264,41 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:442 counter/models.py:308 pedagogy/models.py:41 +#: accounting/models.py:421 counter/models.py:306 pedagogy/models.py:41 msgid "code" msgstr "code" -#: accounting/models.py:446 +#: accounting/models.py:425 msgid "An accounting type code contains only numbers" msgstr "Un code comptable ne contient que des numéros" -#: accounting/models.py:452 +#: accounting/models.py:431 msgid "movement type" msgstr "type de mouvement" -#: accounting/models.py:454 +#: accounting/models.py:433 #: accounting/templates/accounting/journal_statement_nature.jinja:9 #: accounting/templates/accounting/journal_statement_person.jinja:12 #: accounting/views.py:549 msgid "Credit" msgstr "Crédit" -#: accounting/models.py:455 +#: accounting/models.py:434 #: accounting/templates/accounting/journal_statement_nature.jinja:28 #: accounting/templates/accounting/journal_statement_person.jinja:40 #: accounting/views.py:549 msgid "Debit" msgstr "Débit" -#: accounting/models.py:456 +#: accounting/models.py:435 msgid "Neutral" msgstr "Neutre" -#: accounting/models.py:487 +#: accounting/models.py:464 msgid "simplified accounting types" msgstr "type simplifié" -#: accounting/models.py:492 +#: accounting/models.py:469 msgid "simplified type" msgstr "type simplifié" @@ -375,17 +375,17 @@ msgstr "Compte en banque : " #: election/templates/election/election_detail.jinja:187 #: forum/templates/forum/macros.jinja:21 #: launderette/templates/launderette/launderette_admin.jinja:16 -#: launderette/views.py:217 pedagogy/templates/pedagogy/guide.jinja:99 +#: launderette/views.py:210 pedagogy/templates/pedagogy/guide.jinja:99 #: pedagogy/templates/pedagogy/guide.jinja:114 #: pedagogy/templates/pedagogy/uv_detail.jinja:189 -#: sas/templates/sas/album.jinja:32 sas/templates/sas/moderation.jinja:18 -#: sas/templates/sas/picture.jinja:50 trombi/templates/trombi/detail.jinja:35 +#: sas/templates/sas/album.jinja:36 sas/templates/sas/moderation.jinja:18 +#: sas/templates/sas/picture.jinja:70 trombi/templates/trombi/detail.jinja:35 #: trombi/templates/trombi/edit_profile.jinja:35 msgid "Delete" msgstr "Supprimer" #: accounting/templates/accounting/bank_account_details.jinja:18 -#: club/views.py:79 core/views/user.py:200 sas/templates/sas/picture.jinja:72 +#: club/views.py:79 core/views/user.py:202 sas/templates/sas/picture.jinja:90 msgid "Infos" msgstr "Infos" @@ -419,7 +419,7 @@ msgstr "Nouveau compte club" #: com/templates/com/weekmail.jinja:61 core/templates/core/file.jinja:38 #: 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:230 +#: core/templates/core/user_tools.jinja:71 core/views/user.py:232 #: counter/templates/counter/cash_summary_list.jinja:53 #: counter/templates/counter/counter_list.jinja:17 #: counter/templates/counter/counter_list.jinja:33 @@ -430,7 +430,7 @@ msgstr "Nouveau compte club" #: pedagogy/templates/pedagogy/guide.jinja:98 #: pedagogy/templates/pedagogy/guide.jinja:113 #: pedagogy/templates/pedagogy/uv_detail.jinja:188 -#: sas/templates/sas/album.jinja:31 trombi/templates/trombi/detail.jinja:9 +#: sas/templates/sas/album.jinja:35 trombi/templates/trombi/detail.jinja:9 #: trombi/templates/trombi/edit_profile.jinja:34 msgid "Edit" msgstr "Éditer" @@ -616,7 +616,7 @@ msgstr "No" #: 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/views.py:310 +#: rootplace/templates/rootplace/logs.jinja:12 sas/forms.py:90 #: trombi/templates/trombi/user_profile.jinja:40 msgid "Date" msgstr "Date" @@ -650,7 +650,7 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:944 +#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:955 #: pedagogy/templates/pedagogy/moderation.jinja:13 #: pedagogy/templates/pedagogy/uv_detail.jinja:142 #: trombi/templates/trombi/comment.jinja:4 @@ -783,7 +783,7 @@ msgstr "Sauver" #: accounting/templates/accounting/refound_account.jinja:4 #: accounting/templates/accounting/refound_account.jinja:9 -#: accounting/views.py:868 +#: accounting/views.py:863 msgid "Refound account" msgstr "Remboursement de compte" @@ -876,15 +876,15 @@ msgstr "Commentaire :" msgid "Signature:" msgstr "Signature :" -#: accounting/views.py:663 +#: accounting/views.py:661 msgid "General statement" msgstr "Bilan général" -#: accounting/views.py:670 +#: accounting/views.py:668 msgid "No label operations" msgstr "Opérations sans étiquette" -#: accounting/views.py:826 +#: accounting/views.py:821 msgid "Refound this account" msgstr "Rembourser ce compte" @@ -930,7 +930,7 @@ msgstr "S'abonner" msgid "Remove" msgstr "Retirer" -#: club/forms.py:70 launderette/views.py:219 +#: club/forms.py:70 launderette/views.py:212 #: pedagogy/templates/pedagogy/moderation.jinja:15 msgid "Action" msgstr "Action" @@ -959,23 +959,23 @@ msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" msgid "Begin date" msgstr "Date de début" -#: club/forms.py:156 com/views.py:83 com/views.py:202 counter/forms.py:194 -#: election/views.py:167 subscription/views.py:38 +#: club/forms.py:156 com/views.py:83 com/views.py:201 counter/forms.py:194 +#: election/views.py:171 subscription/views.py:38 msgid "End date" msgstr "Date de fin" #: club/forms.py:160 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.py:141 +#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:137 msgid "Counter" msgstr "Comptoir" -#: club/forms.py:167 counter/views.py:688 +#: club/forms.py:167 counter/views.py:683 msgid "Products" msgstr "Produits" -#: club/forms.py:172 counter/views.py:693 +#: club/forms.py:172 counter/views.py:688 msgid "Archived products" msgstr "Produits archivés" @@ -997,7 +997,7 @@ msgstr "Vous ne pouvez pas ajouter deux fois le même utilisateur" msgid "You should specify a role" msgstr "Vous devez choisir un rôle" -#: club/forms.py:283 sas/views.py:117 sas/views.py:241 +#: club/forms.py:283 sas/views.py:58 sas/views.py:176 msgid "You do not have the permission to do that" msgstr "Vous n'avez pas la permission de faire cela" @@ -1045,58 +1045,58 @@ msgstr "Vous ne pouvez pas faire de boucles dans les clubs" 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:935 counter/models.py:971 -#: eboutic/models.py:53 eboutic/models.py:185 election/models.py:183 -#: launderette/models.py:136 launderette/models.py:198 sas/models.py:274 -#: trombi/models.py:206 +#: club/models.py:337 counter/models.py:920 counter/models.py:956 +#: eboutic/models.py:53 eboutic/models.py:189 election/models.py:183 +#: launderette/models.py:130 launderette/models.py:184 sas/models.py:273 +#: trombi/models.py:205 msgid "user" msgstr "nom d'utilisateur" #: club/models.py:354 core/models.py:356 election/models.py:178 -#: election/models.py:212 trombi/models.py:211 +#: election/models.py:212 trombi/models.py:210 msgid "role" msgstr "rôle" #: club/models.py:359 core/models.py:89 counter/models.py:266 -#: counter/models.py:299 election/models.py:13 election/models.py:115 -#: election/models.py:188 forum/models.py:60 forum/models.py:244 +#: counter/models.py:297 election/models.py:13 election/models.py:115 +#: election/models.py:188 forum/models.py:61 forum/models.py:245 msgid "description" msgstr "description" -#: club/models.py:417 club/models.py:513 +#: club/models.py:415 club/models.py:511 msgid "Email address" msgstr "Adresse email" -#: club/models.py:425 +#: club/models.py:423 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:429 com/models.py:82 com/models.py:309 core/models.py:960 +#: club/models.py:427 com/models.py:82 com/models.py:309 core/models.py:946 msgid "is moderated" msgstr "est modéré" -#: club/models.py:433 com/models.py:86 com/models.py:313 +#: club/models.py:431 com/models.py:86 com/models.py:313 msgid "moderator" msgstr "modérateur" -#: club/models.py:460 +#: club/models.py:458 msgid "This mailing list already exists." msgstr "Cette liste de diffusion existe déjà." -#: club/models.py:499 club/templates/club/mailing.jinja:23 +#: club/models.py:497 club/templates/club/mailing.jinja:23 msgid "Mailing" msgstr "Liste de diffusion" -#: club/models.py:523 +#: club/models.py:521 msgid "At least user or email is required" msgstr "Au moins un utilisateur ou un email est nécessaire" -#: club/models.py:531 club/tests.py:769 +#: club/models.py:529 club/tests.py:769 msgid "This email is already suscribed in this mailing" msgstr "Cet email est déjà abonné à cette mailing" -#: club/models.py:559 +#: club/models.py:557 msgid "Unregistered user" msgstr "Utilisateur non enregistré" @@ -1151,7 +1151,7 @@ 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:312 -#: launderette/views.py:217 trombi/templates/trombi/detail.jinja:19 +#: launderette/views.py:210 trombi/templates/trombi/detail.jinja:19 msgid "Add" msgstr "Ajouter" @@ -1240,8 +1240,8 @@ msgstr "Quantité" #: 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:40 -#: subscription/templates/subscription/stats.jinja:48 +#: subscription/templates/subscription/stats.jinja:42 +#: subscription/templates/subscription/stats.jinja:50 msgid "Total" msgstr "Total" @@ -1373,8 +1373,8 @@ msgstr "Anciens membres" msgid "History" msgstr "Historique" -#: club/views.py:116 core/templates/core/base.jinja:107 core/views/user.py:223 -#: sas/templates/sas/picture.jinja:91 trombi/views.py:61 +#: club/views.py:116 core/templates/core/base.jinja:105 core/views/user.py:225 +#: sas/templates/sas/picture.jinja:109 trombi/views.py:62 msgid "Tools" msgstr "Outils" @@ -1382,7 +1382,7 @@ msgstr "Outils" msgid "Edit club page" msgstr "Éditer la page de club" -#: club/views.py:145 club/views.py:451 +#: club/views.py:145 club/views.py:452 msgid "Sellings" msgstr "Vente" @@ -1390,7 +1390,7 @@ msgstr "Vente" msgid "Mailing list" msgstr "Listes de diffusion" -#: club/views.py:161 com/views.py:134 +#: club/views.py:161 com/views.py:133 msgid "Posters list" msgstr "Liste d'affiches" @@ -1429,8 +1429,8 @@ msgid "Call" msgstr "Appel" #: com/models.py:67 com/models.py:174 com/models.py:248 election/models.py:12 -#: election/models.py:114 election/models.py:152 forum/models.py:255 -#: forum/models.py:309 pedagogy/models.py:96 +#: election/models.py:114 election/models.py:152 forum/models.py:256 +#: forum/models.py:310 pedagogy/models.py:97 msgid "title" msgstr "titre" @@ -1438,17 +1438,17 @@ msgstr "titre" msgid "summary" msgstr "résumé" -#: com/models.py:69 com/models.py:249 trombi/models.py:189 +#: com/models.py:69 com/models.py:249 trombi/models.py:188 msgid "content" msgstr "contenu" -#: com/models.py:71 core/models.py:1522 launderette/models.py:92 -#: launderette/models.py:130 launderette/models.py:181 +#: com/models.py:71 core/models.py:1499 launderette/models.py:88 +#: launderette/models.py:124 launderette/models.py:167 msgid "type" msgstr "type" -#: com/models.py:79 com/models.py:253 pedagogy/models.py:56 -#: pedagogy/models.py:199 trombi/models.py:179 +#: com/models.py:79 com/models.py:253 pedagogy/models.py:57 +#: pedagogy/models.py:200 trombi/models.py:178 msgid "author" msgstr "auteur" @@ -1492,7 +1492,7 @@ msgstr "weekmail" msgid "rank" msgstr "rang" -#: com/models.py:295 core/models.py:925 core/models.py:975 +#: com/models.py:295 core/models.py:911 core/models.py:961 msgid "file" msgstr "fichier" @@ -1504,7 +1504,7 @@ msgstr "temps d'affichage" 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 +#: com/templates/com/mailing_admin.jinja:4 com/views.py:126 #: core/templates/core/user_tools.jinja:136 msgid "Mailing lists administration" msgstr "Administration des mailing listes" @@ -1517,7 +1517,7 @@ msgstr "Administration des mailing listes" #: com/templates/com/news_detail.jinja:39 #: core/templates/core/file_detail.jinja:65 #: core/templates/core/file_moderation.jinja:23 -#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:47 +#: sas/templates/sas/moderation.jinja:17 sas/templates/sas/picture.jinja:67 msgid "Moderate" msgstr "Modérer" @@ -1574,7 +1574,7 @@ msgstr "Informations affichées" #: 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:224 +#: launderette/views.py:217 msgid "Type" msgstr "Type" @@ -1613,7 +1613,7 @@ msgstr "Résumé" #: 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 +#: forum/templates/forum/forum.jinja:55 sas/models.py:297 msgid "Author" msgstr "Auteur" @@ -1659,7 +1659,7 @@ msgid "Calls to moderate" msgstr "Appels à modérer" #: com/templates/com/news_admin_list.jinja:242 -#: core/templates/core/base.jinja:222 +#: core/templates/core/base.jinja:220 msgid "Events" msgstr "Événements" @@ -1819,7 +1819,7 @@ 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/views.py:103 core/templates/core/user_tools.jinja:129 msgid "Weekmail" msgstr "Weekmail" @@ -1862,7 +1862,7 @@ 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:217 +#: core/templates/core/user_account_detail.jinja:116 launderette/views.py:210 #: pedagogy/templates/pedagogy/uv_detail.jinja:16 #: pedagogy/templates/pedagogy/uv_detail.jinja:25 #: trombi/templates/trombi/comment_moderation.jinja:10 @@ -1919,56 +1919,56 @@ msgstr "Le mot de la fin" msgid "Format: 16:9 | Resolution: 1920x1080" msgstr "Format : 16:9 | Résolution : 1920x1080" -#: com/views.py:77 com/views.py:199 election/views.py:164 +#: com/views.py:77 com/views.py:198 election/views.py:168 #: subscription/views.py:35 msgid "Start date" msgstr "Date de début" -#: com/views.py:99 +#: com/views.py:98 msgid "Communication administration" msgstr "Administration de la communication" -#: com/views.py:110 core/templates/core/user_tools.jinja:130 +#: com/views.py:109 core/templates/core/user_tools.jinja:130 msgid "Weekmail destinations" msgstr "Destinataires du Weekmail" -#: com/views.py:114 +#: com/views.py:113 msgid "Info message" msgstr "Message d'info" -#: com/views.py:120 +#: com/views.py:119 msgid "Alert message" msgstr "Message d'alerte" -#: com/views.py:141 +#: com/views.py:140 msgid "Screens list" msgstr "Liste d'écrans" -#: com/views.py:204 +#: com/views.py:203 msgid "Until" msgstr "Jusqu'à" -#: com/views.py:206 +#: com/views.py:205 msgid "Automoderation" msgstr "Automodération" -#: com/views.py:213 com/views.py:217 com/views.py:231 +#: com/views.py:212 com/views.py:216 com/views.py:230 msgid "This field is required." msgstr "Ce champ est obligatoire." -#: com/views.py:227 +#: com/views.py:226 msgid "You crazy? You can not finish an event before starting it." msgstr "T'es fou? Un événement ne peut pas finir avant même de commencer." -#: com/views.py:451 +#: com/views.py:450 msgid "Delete and save to regenerate" msgstr "Supprimer et sauver pour régénérer" -#: com/views.py:466 +#: com/views.py:465 msgid "Weekmail of the " msgstr "Weekmail du " -#: com/views.py:570 +#: com/views.py:569 msgid "" "You must be a board member of the selected club to post in the Weekmail." msgstr "" @@ -2166,7 +2166,7 @@ msgstr "département" msgid "dpt option" msgstr "Filière" -#: core/models.py:380 pedagogy/models.py:69 pedagogy/models.py:293 +#: core/models.py:380 pedagogy/models.py:70 pedagogy/models.py:294 msgid "semester" msgstr "semestre" @@ -2206,7 +2206,7 @@ msgstr "profil visible par les cotisants" msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:756 core/templates/core/macros.jinja:75 +#: core/models.py:750 core/templates/core/macros.jinja:75 #: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78 #: core/templates/core/user_detail.jinja:100 #: core/templates/core/user_detail.jinja:101 @@ -2226,101 +2226,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgid "Profile" msgstr "Profil" -#: core/models.py:875 +#: core/models.py:861 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:882 +#: core/models.py:868 msgid "receive the Weekmail" msgstr "recevoir le Weekmail" -#: core/models.py:883 +#: core/models.py:869 msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:885 +#: core/models.py:871 msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:888 +#: core/models.py:874 msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:914 sas/views.py:309 +#: core/models.py:900 sas/forms.py:89 msgid "file name" msgstr "nom du fichier" -#: core/models.py:918 core/models.py:1271 +#: core/models.py:904 core/models.py:1257 msgid "parent" msgstr "parent" -#: core/models.py:932 +#: core/models.py:918 msgid "compressed file" msgstr "version allégée" -#: core/models.py:939 +#: core/models.py:925 msgid "thumbnail" msgstr "miniature" -#: core/models.py:947 core/models.py:964 +#: core/models.py:933 core/models.py:950 msgid "owner" msgstr "propriétaire" -#: core/models.py:951 core/models.py:1288 core/views/files.py:223 +#: core/models.py:937 core/models.py:1274 core/views/files.py:223 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:954 core/models.py:1291 core/views/files.py:226 +#: core/models.py:940 core/models.py:1277 core/views/files.py:226 msgid "view group" msgstr "groupe de vue" -#: core/models.py:956 +#: core/models.py:942 msgid "is folder" msgstr "est un dossier" -#: core/models.py:957 +#: core/models.py:943 msgid "mime type" msgstr "type mime" -#: core/models.py:958 +#: core/models.py:944 msgid "size" msgstr "taille" -#: core/models.py:969 +#: core/models.py:955 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:971 +#: core/models.py:957 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:1040 +#: core/models.py:1026 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:1042 core/models.py:1046 +#: core/models.py:1028 core/models.py:1032 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:1049 +#: core/models.py:1035 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:1060 +#: core/models.py:1046 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:1077 +#: core/models.py:1063 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1254 +#: core/models.py:1240 msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1260 +#: core/models.py:1246 msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2328,55 +2328,55 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1278 +#: core/models.py:1264 msgid "page name" msgstr "nom de la page" -#: core/models.py:1283 +#: core/models.py:1269 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1296 +#: core/models.py:1282 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1303 +#: core/models.py:1289 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1353 +#: core/models.py:1339 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1356 +#: core/models.py:1342 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1476 +#: core/models.py:1453 msgid "revision" msgstr "révision" -#: core/models.py:1477 +#: core/models.py:1454 msgid "page title" msgstr "titre de la page" -#: core/models.py:1478 +#: core/models.py:1455 msgid "page content" msgstr "contenu de la page" -#: core/models.py:1519 +#: core/models.py:1496 msgid "url" msgstr "url" -#: core/models.py:1520 +#: core/models.py:1497 msgid "param" msgstr "param" -#: core/models.py:1525 +#: core/models.py:1502 msgid "viewed" msgstr "vue" -#: core/models.py:1583 +#: core/models.py:1560 msgid "operation type" msgstr "type d'opération" @@ -2388,7 +2388,7 @@ msgstr "403, Non autorisé" msgid "404, Not Found" msgstr "404. Non trouvé" -#: core/templates/core/500.jinja:11 +#: core/templates/core/500.jinja:9 msgid "500, Server Error" msgstr "500, Erreur Serveur" @@ -2396,18 +2396,18 @@ msgstr "500, Erreur Serveur" msgid "Welcome!" msgstr "Bienvenue !" -#: core/templates/core/base.jinja:59 core/templates/core/login.jinja:8 +#: core/templates/core/base.jinja:57 core/templates/core/login.jinja:8 #: core/templates/core/login.jinja:18 core/templates/core/login.jinja:51 #: core/templates/core/password_reset_complete.jinja:5 msgid "Login" msgstr "Connexion" -#: core/templates/core/base.jinja:60 core/templates/core/register.jinja:7 +#: core/templates/core/base.jinja:58 core/templates/core/register.jinja:7 #: core/templates/core/register.jinja:16 core/templates/core/register.jinja:22 msgid "Register" msgstr "Inscription" -#: core/templates/core/base.jinja:66 core/templates/core/base.jinja:67 +#: core/templates/core/base.jinja:64 core/templates/core/base.jinja:65 #: forum/templates/forum/macros.jinja:179 #: forum/templates/forum/macros.jinja:183 #: matmat/templates/matmat/search_form.jinja:39 @@ -2416,52 +2416,52 @@ msgstr "Inscription" msgid "Search" msgstr "Recherche" -#: core/templates/core/base.jinja:108 +#: core/templates/core/base.jinja:106 msgid "Logout" msgstr "Déconnexion" -#: core/templates/core/base.jinja:156 +#: core/templates/core/base.jinja:154 msgid "You do not have any unread notification" msgstr "Vous n'avez aucune notification non lue" -#: core/templates/core/base.jinja:161 +#: core/templates/core/base.jinja:159 msgid "View more" msgstr "Voir plus" -#: core/templates/core/base.jinja:164 +#: core/templates/core/base.jinja:162 #: forum/templates/forum/last_unread.jinja:21 msgid "Mark all as read" msgstr "Marquer tout comme lu" -#: core/templates/core/base.jinja:212 +#: core/templates/core/base.jinja:210 msgid "Main" msgstr "Accueil" -#: core/templates/core/base.jinja:214 +#: core/templates/core/base.jinja:212 msgid "Associations & Clubs" msgstr "Associations & Clubs" -#: core/templates/core/base.jinja:216 +#: core/templates/core/base.jinja:214 msgid "AE" msgstr "L'AE" -#: core/templates/core/base.jinja:217 +#: core/templates/core/base.jinja:215 msgid "AE's clubs" msgstr "Les clubs de L'AE" -#: core/templates/core/base.jinja:218 +#: core/templates/core/base.jinja:216 msgid "Others UTBM's Associations" msgstr "Les autres associations de l'UTBM" -#: core/templates/core/base.jinja:224 core/templates/core/user_tools.jinja:172 +#: core/templates/core/base.jinja:222 core/templates/core/user_tools.jinja:172 msgid "Elections" msgstr "Élections" -#: core/templates/core/base.jinja:225 +#: core/templates/core/base.jinja:223 msgid "Big event" msgstr "Grandes Activités" -#: core/templates/core/base.jinja:228 +#: core/templates/core/base.jinja:226 #: 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 @@ -2470,11 +2470,11 @@ msgstr "Grandes Activités" msgid "Forum" msgstr "Forum" -#: core/templates/core/base.jinja:229 +#: core/templates/core/base.jinja:227 msgid "Gallery" msgstr "Photos" -#: core/templates/core/base.jinja:230 counter/models.py:466 +#: core/templates/core/base.jinja:228 counter/models.py:459 #: counter/templates/counter/counter_list.jinja:11 #: eboutic/templates/eboutic/eboutic_main.jinja:4 #: eboutic/templates/eboutic/eboutic_main.jinja:22 @@ -2484,75 +2484,75 @@ msgstr "Photos" msgid "Eboutic" msgstr "Eboutic" -#: core/templates/core/base.jinja:232 +#: core/templates/core/base.jinja:230 msgid "Services" msgstr "Services" -#: core/templates/core/base.jinja:234 +#: core/templates/core/base.jinja:232 msgid "Matmatronch" msgstr "Matmatronch" -#: core/templates/core/base.jinja:235 launderette/models.py:38 +#: core/templates/core/base.jinja:233 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 msgid "Launderette" msgstr "Laverie" -#: core/templates/core/base.jinja:236 core/templates/core/file.jinja:20 +#: core/templates/core/base.jinja:234 core/templates/core/file.jinja:20 #: core/views/files.py:116 msgid "Files" msgstr "Fichiers" -#: core/templates/core/base.jinja:237 core/templates/core/user_tools.jinja:163 +#: core/templates/core/base.jinja:235 core/templates/core/user_tools.jinja:163 msgid "Pedagogy" msgstr "Pédagogie" -#: core/templates/core/base.jinja:241 +#: core/templates/core/base.jinja:239 msgid "My Benefits" msgstr "Mes Avantages" -#: core/templates/core/base.jinja:243 +#: core/templates/core/base.jinja:241 msgid "Sponsors" msgstr "Partenaires" -#: core/templates/core/base.jinja:244 +#: core/templates/core/base.jinja:242 msgid "Subscriber benefits" msgstr "Les avantages cotisants" -#: core/templates/core/base.jinja:248 +#: core/templates/core/base.jinja:246 msgid "Help" msgstr "Aide" -#: core/templates/core/base.jinja:250 +#: core/templates/core/base.jinja:248 msgid "FAQ" msgstr "FAQ" -#: core/templates/core/base.jinja:251 core/templates/core/base.jinja:291 +#: core/templates/core/base.jinja:249 core/templates/core/base.jinja:289 msgid "Contacts" msgstr "Contacts" -#: core/templates/core/base.jinja:252 +#: core/templates/core/base.jinja:250 msgid "Wiki" msgstr "Wiki" -#: core/templates/core/base.jinja:292 +#: core/templates/core/base.jinja:290 msgid "Legal notices" msgstr "Mentions légales" -#: core/templates/core/base.jinja:293 +#: core/templates/core/base.jinja:291 msgid "Intellectual property" msgstr "Propriété intellectuelle" -#: core/templates/core/base.jinja:294 +#: core/templates/core/base.jinja:292 msgid "Help & Documentation" msgstr "Aide & Documentation" -#: core/templates/core/base.jinja:295 +#: core/templates/core/base.jinja:293 msgid "R&D" msgstr "R&D" -#: core/templates/core/base.jinja:298 +#: core/templates/core/base.jinja:296 msgid "Site created by the IT Department of the AE" msgstr "Site réalisé par le Pôle Informatique de l'AE" @@ -2582,6 +2582,7 @@ msgstr "Confirmation" #: core/templates/core/delete_confirm.jinja:20 #: core/templates/core/file_delete_confirm.jinja:14 #: counter/templates/counter/counter_click.jinja:121 +#: sas/templates/sas/ask_picture_removal.jinja:20 msgid "Cancel" msgstr "Annuler" @@ -2614,24 +2615,24 @@ msgstr "Propriétés" #: core/templates/core/file_detail.jinja:13 #: core/templates/core/file_moderation.jinja:20 -#: sas/templates/sas/picture.jinja:84 +#: sas/templates/sas/picture.jinja:102 msgid "Owner: " msgstr "Propriétaire : " -#: core/templates/core/file_detail.jinja:26 sas/templates/sas/album.jinja:46 +#: core/templates/core/file_detail.jinja:26 sas/templates/sas/album.jinja:50 #: sas/templates/sas/main.jinja:49 msgid "Clear clipboard" msgstr "Vider le presse-papier" -#: core/templates/core/file_detail.jinja:27 sas/templates/sas/album.jinja:33 +#: core/templates/core/file_detail.jinja:27 sas/templates/sas/album.jinja:37 msgid "Cut" msgstr "Couper" -#: core/templates/core/file_detail.jinja:28 sas/templates/sas/album.jinja:34 +#: core/templates/core/file_detail.jinja:28 sas/templates/sas/album.jinja:38 msgid "Paste" msgstr "Coller" -#: core/templates/core/file_detail.jinja:31 sas/templates/sas/album.jinja:40 +#: core/templates/core/file_detail.jinja:31 sas/templates/sas/album.jinja:44 #: sas/templates/sas/main.jinja:43 msgid "Clipboard: " msgstr "Presse-papier : " @@ -2642,7 +2643,7 @@ msgstr "Nom réel : " #: core/templates/core/file_detail.jinja:54 #: core/templates/core/file_moderation.jinja:21 -#: sas/templates/sas/picture.jinja:75 +#: sas/templates/sas/picture.jinja:93 msgid "Date: " msgstr "Date : " @@ -2759,7 +2760,7 @@ msgstr "Cotisant jusqu'au %(subscription_end)s" msgid "Account number: " msgstr "Numéro de compte : " -#: core/templates/core/macros.jinja:91 launderette/models.py:202 +#: core/templates/core/macros.jinja:91 launderette/models.py:188 msgid "Slot" msgstr "Créneau" @@ -2998,7 +2999,7 @@ msgstr "Résultat de la recherche" msgid "Users" msgstr "Utilisateurs" -#: core/templates/core/search.jinja:20 core/views/user.py:245 +#: core/templates/core/search.jinja:20 core/views/user.py:247 msgid "Clubs" msgstr "Clubs" @@ -3039,11 +3040,11 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:54 -#: core/templates/core/user_tools.jinja:58 counter/views.py:713 +#: core/templates/core/user_tools.jinja:58 counter/views.py:708 msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:638 +#: core/templates/core/user_account.jinja:69 core/views/user.py:639 msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" @@ -3151,7 +3152,7 @@ msgid "Subscription end" msgstr "Fin de la cotisation" #: core/templates/core/user_detail.jinja:185 -#: subscription/templates/subscription/stats.jinja:36 +#: subscription/templates/subscription/stats.jinja:38 msgid "Subscription type" msgstr "Type de cotisation" @@ -3265,13 +3266,13 @@ msgstr "Photos de %(user_name)s" msgid "Download all my pictures" msgstr "Télécharger toutes mes photos" -#: core/templates/core/user_pictures.jinja:45 sas/templates/sas/album.jinja:74 +#: core/templates/core/user_pictures.jinja:45 sas/templates/sas/album.jinja:78 #: sas/templates/sas/macros.jinja:16 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:237 +#: core/templates/core/user_preferences.jinja:13 core/views/user.py:239 msgid "Preferences" msgstr "Préférences" @@ -3279,7 +3280,7 @@ msgstr "Préférences" msgid "General" msgstr "Général" -#: core/templates/core/user_preferences.jinja:21 trombi/views.py:56 +#: core/templates/core/user_preferences.jinja:21 trombi/views.py:57 msgid "Trombi" msgstr "Trombi" @@ -3346,7 +3347,7 @@ msgstr "Outils utilisateurs" msgid "Sith management" msgstr "Gestion de Sith" -#: core/templates/core/user_tools.jinja:21 core/views/user.py:253 +#: core/templates/core/user_tools.jinja:21 core/views/user.py:255 msgid "Groups" msgstr "Groupes" @@ -3375,7 +3376,7 @@ msgid "Subscription stats" msgstr "Statistiques de cotisation" #: core/templates/core/user_tools.jinja:48 counter/forms.py:164 -#: counter/views.py:683 +#: counter/views.py:678 msgid "Counters" msgstr "Comptoirs" @@ -3392,16 +3393,16 @@ 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.py:703 +#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:698 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.py:708 +#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:703 msgid "Invoices call" msgstr "Appels à facture" -#: core/templates/core/user_tools.jinja:72 core/views/user.py:272 +#: core/templates/core/user_tools.jinja:72 core/views/user.py:277 #: counter/templates/counter/counter_list.jinja:18 #: counter/templates/counter/counter_list.jinja:34 #: counter/templates/counter/counter_list.jinja:50 @@ -3496,12 +3497,12 @@ 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:153 core/views/forms.py:277 core/views/forms.py:284 -#: sas/views.py:81 +#: sas/forms.py:60 #, 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:228 sas/views.py:313 +#: core/views/files.py:228 sas/forms.py:93 msgid "Apply rights recursively" msgstr "Appliquer les droits récursivement" @@ -3549,7 +3550,7 @@ msgstr "Parrain / Marraine" msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:315 counter/forms.py:72 trombi/views.py:149 +#: core/views/forms.py:315 counter/forms.py:72 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3572,12 +3573,12 @@ msgid "%s is already your godchild" msgstr "%s est déjà votre fillot/fillote" #: core/views/forms.py:359 core/views/forms.py:377 election/models.py:22 -#: election/views.py:147 +#: election/views.py:151 msgid "edit groups" msgstr "groupe d'édition" #: core/views/forms.py:362 core/views/forms.py:380 election/models.py:29 -#: election/views.py:150 +#: election/views.py:154 msgid "view groups" msgstr "groupe de vue" @@ -3589,26 +3590,26 @@ msgstr "Utilisateurs à retirer du groupe" msgid "Users to add to group" msgstr "Utilisateurs à ajouter au groupe" -#: core/views/user.py:182 +#: core/views/user.py:184 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:205 +#: core/views/user.py:207 msgid "Family" msgstr "Famille" -#: core/views/user.py:210 sas/templates/sas/album.jinja:63 +#: core/views/user.py:212 sas/templates/sas/album.jinja:67 #: trombi/templates/trombi/export.jinja:25 #: trombi/templates/trombi/user_profile.jinja:11 msgid "Pictures" msgstr "Photos" -#: core/views/user.py:218 +#: core/views/user.py:220 msgid "Galaxy" msgstr "Galaxie" -#: counter/apps.py:30 counter/models.py:482 counter/models.py:941 -#: counter/models.py:977 launderette/models.py:32 +#: counter/apps.py:30 counter/models.py:475 counter/models.py:926 +#: counter/models.py:962 launderette/models.py:32 msgid "counter" msgstr "comptoir" @@ -3628,7 +3629,7 @@ msgstr "Produit parent" msgid "Buying groups" msgstr "Groupes d'achat" -#: counter/management/commands/dump_warning_mail.py:82 +#: counter/management/commands/dump_warning_mail.py:113 msgid "Clearing of your AE account" msgstr "Vidange de votre compte AE" @@ -3652,7 +3653,7 @@ msgstr "client" msgid "customers" msgstr "clients" -#: counter/models.py:74 counter/views.py:265 +#: counter/models.py:74 counter/views.py:261 msgid "Not enough money" msgstr "Solde insuffisant" @@ -3700,117 +3701,117 @@ msgstr "Mettre à True si le mail a reçu une erreur" msgid "The operation that emptied the account." msgstr "L'opération qui a vidé le compte." -#: counter/models.py:277 counter/models.py:303 +#: counter/models.py:277 counter/models.py:301 msgid "product type" msgstr "type du produit" -#: counter/models.py:309 +#: counter/models.py:307 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:310 +#: counter/models.py:308 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:311 +#: counter/models.py:309 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:318 +#: counter/models.py:316 msgid "icon" msgstr "icône" -#: counter/models.py:323 +#: counter/models.py:321 msgid "limit age" msgstr "âge limite" -#: counter/models.py:324 +#: counter/models.py:322 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:328 +#: counter/models.py:326 msgid "parent product" msgstr "produit parent" -#: counter/models.py:334 +#: counter/models.py:332 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:336 election/models.py:50 +#: counter/models.py:334 election/models.py:50 msgid "archived" msgstr "archivé" -#: counter/models.py:339 counter/models.py:1077 +#: counter/models.py:337 counter/models.py:1060 msgid "product" msgstr "produit" -#: counter/models.py:461 +#: counter/models.py:454 msgid "products" msgstr "produits" -#: counter/models.py:464 +#: counter/models.py:457 msgid "counter type" msgstr "type de comptoir" -#: counter/models.py:466 +#: counter/models.py:459 msgid "Bar" msgstr "Bar" -#: counter/models.py:466 +#: counter/models.py:459 msgid "Office" msgstr "Bureau" -#: counter/models.py:469 +#: counter/models.py:462 msgid "sellers" msgstr "vendeurs" -#: counter/models.py:477 launderette/models.py:192 +#: counter/models.py:470 launderette/models.py:178 msgid "token" msgstr "jeton" -#: counter/models.py:672 +#: counter/models.py:665 msgid "bank" msgstr "banque" -#: counter/models.py:674 counter/models.py:775 +#: counter/models.py:667 counter/models.py:768 msgid "is validated" msgstr "est validé" -#: counter/models.py:679 +#: counter/models.py:672 msgid "refilling" msgstr "rechargement" -#: counter/models.py:752 eboutic/models.py:245 +#: counter/models.py:745 eboutic/models.py:249 msgid "unit price" msgstr "prix unitaire" -#: counter/models.py:753 counter/models.py:1057 eboutic/models.py:246 +#: counter/models.py:746 counter/models.py:1040 eboutic/models.py:250 msgid "quantity" msgstr "quantité" -#: counter/models.py:772 +#: counter/models.py:765 msgid "Sith account" msgstr "Compte utilisateur" -#: counter/models.py:772 sith/settings.py:412 sith/settings.py:417 +#: counter/models.py:765 sith/settings.py:412 sith/settings.py:417 #: sith/settings.py:437 msgid "Credit card" msgstr "Carte bancaire" -#: counter/models.py:780 +#: counter/models.py:773 msgid "selling" msgstr "vente" -#: counter/models.py:884 +#: counter/models.py:877 msgid "Unknown event" msgstr "Événement inconnu" -#: counter/models.py:885 +#: counter/models.py:878 #, python-format msgid "Eticket bought for the event %(event)s" msgstr "Eticket acheté pour l'événement %(event)s" -#: counter/models.py:887 counter/models.py:910 +#: counter/models.py:880 counter/models.py:893 #, python-format msgid "" "You bought an eticket for the event %(event)s.\n" @@ -3822,108 +3823,121 @@ msgstr "" "Vous pouvez également retrouver tous vos e-tickets sur votre page de compte " "%(url)s." -#: counter/models.py:946 +#: counter/models.py:931 msgid "last activity date" msgstr "dernière activité" -#: counter/models.py:949 +#: counter/models.py:934 msgid "permanency" msgstr "permanence" -#: counter/models.py:982 +#: counter/models.py:967 msgid "emptied" msgstr "coffre vidée" -#: counter/models.py:985 +#: counter/models.py:970 msgid "cash register summary" msgstr "relevé de caisse" -#: counter/models.py:1053 +#: counter/models.py:1036 msgid "cash summary" msgstr "relevé" -#: counter/models.py:1056 +#: counter/models.py:1039 msgid "value" msgstr "valeur" -#: counter/models.py:1059 +#: counter/models.py:1042 msgid "check" msgstr "chèque" -#: counter/models.py:1061 +#: counter/models.py:1044 msgid "True if this is a bank check, else False" msgstr "Vrai si c'est un chèque, sinon Faux." -#: counter/models.py:1065 +#: counter/models.py:1048 msgid "cash register summary item" msgstr "élément de relevé de caisse" -#: counter/models.py:1081 +#: counter/models.py:1064 msgid "banner" msgstr "bannière" -#: counter/models.py:1083 +#: counter/models.py:1066 msgid "event date" msgstr "date de l'événement" -#: counter/models.py:1085 +#: counter/models.py:1068 msgid "event title" msgstr "titre de l'événement" -#: counter/models.py:1087 +#: counter/models.py:1070 msgid "secret" msgstr "secret" -#: counter/models.py:1126 +#: counter/models.py:1109 msgid "uid" msgstr "uid" -#: counter/models.py:1131 +#: counter/models.py:1114 msgid "student cards" msgstr "cartes étudiante" -#: counter/templates/counter/account_dump_warning_mail.jinja:6 +#: counter/templates/counter/account_dump_warning_mail.jinja:1 +msgid "Hello" +msgstr "Bonjour" + +#: counter/templates/counter/account_dump_warning_mail.jinja:3 #, python-format msgid "" -"You received this email because your last subscription to the\n" -" Students' association ended on %(date)s." +"You received this email because your last subscription to the Students' " +"association ended on %(date)s." 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/account_dump_warning_mail.jinja:11 +#: counter/templates/counter/account_dump_warning_mail.jinja:6 #, python-format msgid "" -"In accordance with the Internal Regulations, the balance of any\n" -" inactive AE account for more than 2 years automatically goes back\n" -" to the AE.\n" -" The money present on your account will therefore be recovered in full\n" -" on %(date)s, for a total of %(amount)s €." +"In accordance with the Internal Regulations, the balance of any inactive AE " +"account for more than 2 years automatically goes back to the AE. The money " +"present on your account will therefore be recovered in full on %(date)s, for " +"a total of %(amount)s €." msgstr "" "Conformément au Règlement intérieur, le solde de tout compte AE inactif " "depuis plus de 2 ans revient de droit à l'AE. L'argent présent sur votre " "compte sera donc récupéré en totalité le %(date)s, pour un total de " "%(amount)s €. " -#: counter/templates/counter/account_dump_warning_mail.jinja:19 +#: counter/templates/counter/account_dump_warning_mail.jinja:12 msgid "" -"However, if your subscription is renewed by this date,\n" -" your right to keep the money in your AE account will be renewed." +"However, if your subscription is renewed by this date, your right to keep " +"the money in your AE account will be renewed." 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/account_dump_warning_mail.jinja:25 +#: counter/templates/counter/account_dump_warning_mail.jinja:16 msgid "" -"You can also request a refund by sending an email to\n" -" ae@utbm.fr\n" -" before the aforementioned date." +"You can also request a refund by sending an email to ae@utbm.fr before the " +"aforementioned date." msgstr "" "Vous pouvez également effectuer une demande de remboursement par mail à " -"l'adresse ae@utbm.fr avant la date " -"susmentionnée." +"l'adresse ae@utbm.fr avant la date susmentionnée." -#: counter/templates/counter/account_dump_warning_mail.jinja:32 +#: counter/templates/counter/account_dump_warning_mail.jinja:20 +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 " +"later. If you don't renew your subscription, there will be no consequences " +"other than the loss of the money currently in your AE account." +msgstr "" +"Quel que soit votre décision, vous ne serez pas exclu.e de l'association " +"et vous ne perdrez pas vos droits. Vous serez toujours en mesure de renouveler " +"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/account_dump_warning_mail.jinja:26 msgid "Sincerely" msgstr "Cordialement" @@ -3970,7 +3984,7 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:945 +#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:956 msgid "Emptied" msgstr "Coffre vidé" @@ -3996,8 +4010,8 @@ msgstr "Ce n'est pas un UID de carte étudiante valide" #: 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:141 -#: subscription/templates/subscription/stats.jinja:19 +#: sas/templates/sas/picture.jinja:166 +#: subscription/templates/subscription/stats.jinja:20 msgid "Go" msgstr "Valider" @@ -4127,7 +4141,7 @@ msgid "%(counter_name)s last operations" msgstr "Dernières opérations sur %(counter_name)s" #: counter/templates/counter/product_list.jinja:4 -#: counter/templates/counter/product_list.jinja:12 +#: counter/templates/counter/product_list.jinja:11 msgid "Product list" msgstr "Liste des produits" @@ -4135,11 +4149,11 @@ msgstr "Liste des produits" msgid "New product" msgstr "Nouveau produit" -#: counter/templates/counter/product_list.jinja:21 +#: counter/templates/counter/product_list.jinja:13 msgid "Uncategorized" msgstr "Sans catégorie" -#: counter/templates/counter/product_list.jinja:28 +#: counter/templates/counter/product_list.jinja:20 msgid "There is no products in this website." msgstr "Il n'y a pas de produits dans ce site web." @@ -4196,101 +4210,101 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:151 +#: counter/views.py:147 msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views.py:160 +#: counter/views.py:156 msgid "Last operations" msgstr "Dernières opérations" -#: counter/views.py:207 +#: counter/views.py:203 msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views.py:209 +#: counter/views.py:205 msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views.py:214 +#: counter/views.py:210 msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" -#: counter/views.py:256 +#: counter/views.py:252 msgid "Too young for that product" msgstr "Trop jeune pour ce produit" -#: counter/views.py:259 +#: counter/views.py:255 msgid "Not allowed for that product" msgstr "Non autorisé pour ce produit" -#: counter/views.py:262 +#: counter/views.py:258 msgid "No date of birth provided" msgstr "Pas de date de naissance renseignée" -#: counter/views.py:551 +#: counter/views.py:546 msgid "You have not enough money to buy all the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" -#: counter/views.py:678 +#: counter/views.py:673 msgid "Counter administration" msgstr "Administration des comptoirs" -#: counter/views.py:698 +#: counter/views.py:693 msgid "Product types" msgstr "Types de produit" -#: counter/views.py:902 +#: counter/views.py:913 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:903 +#: counter/views.py:914 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:904 +#: counter/views.py:915 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:905 +#: counter/views.py:916 msgid "1 euro" msgstr "1 €" -#: counter/views.py:906 +#: counter/views.py:917 msgid "2 euros" msgstr "2 €" -#: counter/views.py:907 +#: counter/views.py:918 msgid "5 euros" msgstr "5 €" -#: counter/views.py:908 +#: counter/views.py:919 msgid "10 euros" msgstr "10 €" -#: counter/views.py:909 +#: counter/views.py:920 msgid "20 euros" msgstr "20 €" -#: counter/views.py:910 +#: counter/views.py:921 msgid "50 euros" msgstr "50 €" -#: counter/views.py:912 +#: counter/views.py:923 msgid "100 euros" msgstr "100 €" -#: counter/views.py:915 counter/views.py:921 counter/views.py:927 -#: counter/views.py:933 counter/views.py:939 +#: counter/views.py:926 counter/views.py:932 counter/views.py:938 +#: counter/views.py:944 counter/views.py:950 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:918 counter/views.py:924 counter/views.py:930 -#: counter/views.py:936 counter/views.py:942 +#: counter/views.py:929 counter/views.py:935 counter/views.py:941 +#: counter/views.py:947 counter/views.py:953 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1462 +#: counter/views.py:1473 msgid "people(s)" msgstr "personne(s)" @@ -4307,27 +4321,27 @@ msgstr "Votre panier est vide" 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:190 +#: eboutic/models.py:194 msgid "validated" msgstr "validé" -#: eboutic/models.py:206 +#: eboutic/models.py:210 msgid "Invoice already validated" msgstr "Facture déjà validée" -#: eboutic/models.py:242 +#: eboutic/models.py:246 msgid "product id" msgstr "ID du produit" -#: eboutic/models.py:243 +#: eboutic/models.py:247 msgid "product name" msgstr "nom du produit" -#: eboutic/models.py:244 +#: eboutic/models.py:248 msgid "product type id" msgstr "id du type du produit" -#: eboutic/models.py:261 +#: eboutic/models.py:265 msgid "basket" msgstr "panier" @@ -4456,11 +4470,11 @@ msgstr "début des candidatures" msgid "end candidature" msgstr "fin des candidatures" -#: election/models.py:36 election/views.py:153 +#: election/models.py:36 election/views.py:157 msgid "vote groups" msgstr "groupe de vote" -#: election/models.py:43 election/views.py:160 +#: election/models.py:43 election/views.py:164 msgid "candidature groups" msgstr "groupe de candidature" @@ -4524,7 +4538,7 @@ msgstr "Vous avez déjà soumis votre vote." msgid "You have voted in this election." msgstr "Vous avez déjà voté pour cette élection." -#: election/templates/election/election_detail.jinja:49 election/views.py:86 +#: election/templates/election/election_detail.jinja:49 election/views.py:90 msgid "Blank vote" msgstr "Vote blanc" @@ -4588,87 +4602,87 @@ msgstr "au" msgid "Polls open from" msgstr "Votes ouverts du" -#: election/views.py:37 +#: election/views.py:41 msgid "You have selected too much candidates." msgstr "Vous avez sélectionné trop de candidats." -#: election/views.py:53 +#: election/views.py:57 msgid "User to candidate" msgstr "Utilisateur se présentant" -#: election/views.py:111 +#: election/views.py:115 msgid "This role already exists for this election" msgstr "Ce rôle existe déjà pour cette élection" -#: election/views.py:170 +#: election/views.py:174 msgid "Start candidature" msgstr "Début des candidatures" -#: election/views.py:173 +#: election/views.py:177 msgid "End candidature" msgstr "Fin des candidatures" -#: forum/models.py:61 +#: forum/models.py:62 msgid "is a category" msgstr "est une catégorie" -#: forum/models.py:72 +#: forum/models.py:73 msgid "owner club" msgstr "club propriétaire" -#: forum/models.py:89 +#: forum/models.py:90 msgid "number to choose a specific forum ordering" msgstr "numéro spécifiant l'ordre d'affichage" -#: forum/models.py:94 forum/models.py:251 +#: forum/models.py:95 forum/models.py:252 msgid "the last message" msgstr "le dernier message" -#: forum/models.py:98 +#: forum/models.py:99 msgid "number of topics" msgstr "nombre de sujets" -#: forum/models.py:194 +#: forum/models.py:195 msgid "You can not make loops in forums" msgstr "Vous ne pouvez pas faire de boucles dans les forums" -#: forum/models.py:246 +#: forum/models.py:247 msgid "subscribed users" msgstr "utilisateurs abonnés" -#: forum/models.py:256 +#: forum/models.py:257 msgid "number of messages" msgstr "nombre de messages" -#: forum/models.py:310 +#: forum/models.py:311 msgid "message" msgstr "message" -#: forum/models.py:313 +#: forum/models.py:314 msgid "readers" msgstr "lecteurs" -#: forum/models.py:315 +#: forum/models.py:316 msgid "is deleted" msgstr "est supprimé" -#: forum/models.py:399 +#: forum/models.py:400 msgid "Message edited by" msgstr "Message édité par" -#: forum/models.py:400 +#: forum/models.py:401 msgid "Message deleted by" msgstr "Message supprimé par" -#: forum/models.py:401 +#: forum/models.py:402 msgid "Message undeleted by" msgstr "Message restauré par" -#: forum/models.py:413 +#: forum/models.py:414 msgid "action" msgstr "action" -#: forum/models.py:436 +#: forum/models.py:437 msgid "last read date" msgstr "dernière date de lecture" @@ -4810,31 +4824,31 @@ msgstr "Galaxie de %(user_name)s" msgid "This citizen has not yet joined the galaxy" msgstr "Ce citoyen n'a pas encore rejoint la galaxie" -#: launderette/models.py:88 launderette/models.py:126 +#: launderette/models.py:84 launderette/models.py:120 msgid "launderette" msgstr "laverie" -#: launderette/models.py:94 +#: launderette/models.py:90 msgid "is working" msgstr "fonctionne" -#: launderette/models.py:97 +#: launderette/models.py:93 msgid "Machine" msgstr "Machine" -#: launderette/models.py:132 +#: launderette/models.py:126 msgid "borrow date" msgstr "date d'emprunt" -#: launderette/models.py:143 +#: launderette/models.py:137 msgid "Token" msgstr "Jeton" -#: launderette/models.py:155 +#: launderette/models.py:149 launderette/views.py:262 msgid "Token name can not be blank" msgstr "Le nom du jeton ne peut pas être vide" -#: launderette/models.py:186 +#: launderette/models.py:172 msgid "machine" msgstr "machine" @@ -4863,12 +4877,12 @@ msgid "Washing and drying" msgstr "Lavage et séchage" #: launderette/templates/launderette/launderette_book.jinja:27 -#: sith/settings.py:653 +#: sith/settings.py:655 msgid "Washing" msgstr "Lavage" #: launderette/templates/launderette/launderette_book.jinja:31 -#: sith/settings.py:653 +#: sith/settings.py:655 msgid "Drying" msgstr "Séchage" @@ -4893,25 +4907,25 @@ msgstr "Éditer la page de présentation" msgid "Book launderette slot" msgstr "Réserver un créneau de laverie" -#: launderette/views.py:231 +#: launderette/views.py:224 msgid "Tokens, separated by spaces" msgstr "Jetons, séparés par des espaces" -#: launderette/views.py:251 launderette/views.py:273 +#: launderette/views.py:246 #, python-format msgid "Token %(token_name)s does not exists" msgstr "Le jeton %(token_name)s n'existe pas" -#: launderette/views.py:262 +#: launderette/views.py:258 #, python-format msgid "Token %(token_name)s already exists" msgstr "Un jeton %(token_name)s existe déjà" -#: launderette/views.py:325 +#: launderette/views.py:309 msgid "User has booked no slot" msgstr "L'utilisateur n'a pas réservé de créneau" -#: launderette/views.py:433 +#: launderette/views.py:417 msgid "Token not found" msgstr "Jeton non trouvé" @@ -4964,103 +4978,103 @@ msgstr "" "Le code d'une UV doit seulement contenir des caractères majuscule sans " "accents et nombres" -#: pedagogy/models.py:62 +#: pedagogy/models.py:63 msgid "credit type" msgstr "type de crédit" -#: pedagogy/models.py:67 pedagogy/models.py:97 +#: pedagogy/models.py:68 pedagogy/models.py:98 msgid "uv manager" msgstr "gestionnaire d'uv" -#: pedagogy/models.py:75 +#: pedagogy/models.py:76 msgid "language" msgstr "langue" -#: pedagogy/models.py:81 +#: pedagogy/models.py:82 msgid "credits" msgstr "crédits" -#: pedagogy/models.py:89 +#: pedagogy/models.py:90 msgid "departmenmt" msgstr "département" -#: pedagogy/models.py:98 +#: pedagogy/models.py:99 msgid "objectives" msgstr "objectifs" -#: pedagogy/models.py:99 +#: pedagogy/models.py:100 msgid "program" msgstr "programme" -#: pedagogy/models.py:100 +#: pedagogy/models.py:101 msgid "skills" msgstr "compétences" -#: pedagogy/models.py:101 +#: pedagogy/models.py:102 msgid "key concepts" msgstr "concepts clefs" -#: pedagogy/models.py:106 +#: pedagogy/models.py:107 msgid "hours CM" msgstr "heures CM" -#: pedagogy/models.py:113 +#: pedagogy/models.py:114 msgid "hours TD" msgstr "heures TD" -#: pedagogy/models.py:120 +#: pedagogy/models.py:121 msgid "hours TP" msgstr "heures TP" -#: pedagogy/models.py:127 +#: pedagogy/models.py:128 msgid "hours THE" msgstr "heures THE" -#: pedagogy/models.py:134 +#: pedagogy/models.py:135 msgid "hours TE" msgstr "heures TE" -#: pedagogy/models.py:205 pedagogy/models.py:281 +#: pedagogy/models.py:206 pedagogy/models.py:282 msgid "uv" msgstr "UE" -#: pedagogy/models.py:209 +#: pedagogy/models.py:210 msgid "global grade" msgstr "note globale" -#: pedagogy/models.py:216 +#: pedagogy/models.py:217 msgid "utility grade" msgstr "note d'utilité" -#: pedagogy/models.py:223 +#: pedagogy/models.py:224 msgid "interest grade" msgstr "note d'intérêt" -#: pedagogy/models.py:230 +#: pedagogy/models.py:231 msgid "teaching grade" msgstr "note d'enseignement" -#: pedagogy/models.py:237 +#: pedagogy/models.py:238 msgid "work load grade" msgstr "note de charge de travail" -#: pedagogy/models.py:243 +#: pedagogy/models.py:244 msgid "publish date" msgstr "date de publication" -#: pedagogy/models.py:287 +#: pedagogy/models.py:288 msgid "grade" msgstr "note" -#: pedagogy/models.py:308 +#: pedagogy/models.py:309 msgid "report" msgstr "signaler" -#: pedagogy/models.py:314 +#: pedagogy/models.py:315 msgid "reporter" msgstr "signalant" -#: pedagogy/models.py:317 +#: pedagogy/models.py:318 msgid "reason" msgstr "raison" @@ -5099,7 +5113,7 @@ msgstr "non noté" msgid "UV comment moderation" msgstr "Modération des commentaires d'UV" -#: pedagogy/templates/pedagogy/moderation.jinja:14 +#: pedagogy/templates/pedagogy/moderation.jinja:14 sas/models.py:308 msgid "Reason" msgstr "Raison" @@ -5255,39 +5269,85 @@ msgstr "Fusionner deux utilisateurs" msgid "Merge" msgstr "Fusion" -#: rootplace/views.py:159 +#: rootplace/views.py:160 msgid "User that will be kept" msgstr "Utilisateur qui sera conservé" -#: rootplace/views.py:162 +#: rootplace/views.py:163 msgid "User that will be deleted" msgstr "Utilisateur qui sera supprimé" -#: rootplace/views.py:168 +#: rootplace/views.py:169 msgid "User to be selected" msgstr "Utilisateur à sélectionner" -#: sas/models.py:282 +#: sas/forms.py:16 +msgid "Add a new album" +msgstr "Ajouter un nouvel album" + +#: sas/forms.py:19 +msgid "Upload images" +msgstr "Envoyer les images" + +#: sas/forms.py:37 +#, 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:72 trombi/templates/trombi/detail.jinja:15 +msgid "Add user" +msgstr "Ajouter une personne" + +#: sas/forms.py:117 +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 msgid "picture" msgstr "photo" -#: sas/templates/sas/album.jinja:9 sas/templates/sas/main.jinja:8 -#: sas/templates/sas/main.jinja:17 sas/templates/sas/picture.jinja:12 +#: sas/models.py:303 +msgid "Picture" +msgstr "Photo" + +#: sas/models.py:310 +msgid "Why do you want this image to be removed ?" +msgstr "Pourquoi voulez-vous retirer cette image ?" + +#: sas/models.py:314 +msgid "Picture moderation request" +msgstr "Demande de modération de photo" + +#: sas/models.py:315 +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:14 msgid "SAS" msgstr "SAS" -#: sas/templates/sas/album.jinja:52 sas/templates/sas/moderation.jinja:10 +#: sas/templates/sas/album.jinja:56 sas/templates/sas/moderation.jinja:10 msgid "Albums" msgstr "Albums" -#: sas/templates/sas/album.jinja:96 +#: sas/templates/sas/album.jinja:100 msgid "Upload" msgstr "Envoyer" -#: sas/templates/sas/album.jinja:103 +#: sas/templates/sas/album.jinja:107 msgid "Template generation time: " msgstr "Temps de génération du template : " +#: sas/templates/sas/ask_picture_removal.jinja:9 +msgid "Image removal request" +msgstr "Demande de retrait d'image" + +#: sas/templates/sas/ask_picture_removal.jinja:25 +msgid "Request removal" +msgstr "Demander le retrait" + #: sas/templates/sas/main.jinja:20 msgid "You must be logged in to see the SAS." msgstr "Vous devez être connecté pour voir les photos." @@ -5304,11 +5364,11 @@ msgstr "Toutes les catégories" msgid "SAS moderation" msgstr "Modération du SAS" -#: sas/templates/sas/picture.jinja:35 +#: sas/templates/sas/picture.jinja:37 msgid "Asked for removal" msgstr "Retrait demandé" -#: sas/templates/sas/picture.jinja:38 +#: sas/templates/sas/picture.jinja:40 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." @@ -5317,38 +5377,29 @@ msgstr "" "SAS. Elle sera cachée pour les autres utilisateurs tant qu'elle ne sera pas " "modérée." -#: sas/templates/sas/picture.jinja:95 +#: sas/templates/sas/picture.jinja:48 +msgid "The following issues have been raised:" +msgstr "Les problèmes suivants ont été remontés :" + +#: sas/templates/sas/picture.jinja:113 msgid "HD version" msgstr "Version HD" -#: sas/templates/sas/picture.jinja:98 +#: sas/templates/sas/picture.jinja:117 msgid "Ask for removal" msgstr "Demander le retrait" -#: sas/templates/sas/picture.jinja:118 sas/templates/sas/picture.jinja:129 +#: sas/templates/sas/picture.jinja:138 sas/templates/sas/picture.jinja:149 msgid "Previous picture" msgstr "Image précédente" -#: sas/templates/sas/picture.jinja:137 +#: sas/templates/sas/picture.jinja:157 msgid "People" msgstr "Personne(s)" -#: sas/views.py:37 -msgid "Add a new album" -msgstr "Ajouter un nouvel album" - -#: sas/views.py:40 -msgid "Upload images" -msgstr "Envoyer les images" - -#: sas/views.py:58 -#, python-format -msgid "Error creating album %(album)s: %(msg)s" -msgstr "Erreur de création de l'album %(album)s : %(msg)s" - -#: sas/views.py:93 trombi/templates/trombi/detail.jinja:15 -msgid "Add user" -msgstr "Ajouter une personne" +#: sas/templates/sas/picture.jinja:164 +msgid "Identify users on pictures" +msgstr "Identifiez les utilisateurs sur les photos" #: sith/settings.py:255 sith/settings.py:474 msgid "English" @@ -5526,204 +5577,204 @@ msgstr "Suppression de vente" msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:534 +#: sith/settings.py:536 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:535 +#: sith/settings.py:537 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:537 +#: sith/settings.py:539 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:541 +#: sith/settings.py:543 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:542 +#: sith/settings.py:544 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:543 +#: sith/settings.py:545 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:544 +#: sith/settings.py:546 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:545 +#: sith/settings.py:547 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:546 +#: sith/settings.py:548 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:547 +#: sith/settings.py:549 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:548 +#: sith/settings.py:550 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:550 +#: sith/settings.py:552 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:554 +#: sith/settings.py:556 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:555 +#: sith/settings.py:557 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:556 +#: sith/settings.py:558 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:558 +#: sith/settings.py:560 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:562 +#: sith/settings.py:564 msgid "One day" msgstr "Un jour" -#: sith/settings.py:563 +#: sith/settings.py:565 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:566 +#: sith/settings.py:568 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:571 +#: sith/settings.py:573 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:576 +#: sith/settings.py:578 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:581 +#: sith/settings.py:583 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:586 +#: sith/settings.py:588 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:592 +#: sith/settings.py:594 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:612 +#: sith/settings.py:614 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:613 +#: sith/settings.py:615 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:614 +#: sith/settings.py:616 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:615 +#: sith/settings.py:617 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:616 +#: sith/settings.py:618 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:617 +#: sith/settings.py:619 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:618 +#: sith/settings.py:620 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:619 +#: sith/settings.py:621 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:620 +#: sith/settings.py:622 msgid "Curious" msgstr "Curieux⸱euse" -#: sith/settings.py:657 +#: sith/settings.py:659 msgid "A new poster needs to be moderated" msgstr "Une nouvelle affiche a besoin d'être modérée" -#: sith/settings.py:658 +#: sith/settings.py:660 msgid "A new mailing list needs to be moderated" msgstr "Une nouvelle mailing list a besoin d'être modérée" -#: sith/settings.py:661 +#: sith/settings.py:663 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:663 +#: sith/settings.py:665 #, 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:664 +#: sith/settings.py:666 msgid "New files to be moderated" msgstr "Nouveaux fichiers à modérer" -#: sith/settings.py:665 +#: sith/settings.py:667 #, 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:666 +#: sith/settings.py:668 msgid "You've been identified on some pictures" msgstr "Vous avez été identifié sur des photos" -#: sith/settings.py:667 +#: sith/settings.py:669 #, python-format msgid "You just refilled of %s €" msgstr "Vous avez rechargé votre compte de %s€" -#: sith/settings.py:668 +#: sith/settings.py:670 #, python-format msgid "You just bought %s" msgstr "Vous avez acheté %s" -#: sith/settings.py:669 +#: sith/settings.py:671 msgid "You have a notification" msgstr "Vous avez une notification" -#: sith/settings.py:681 +#: sith/settings.py:683 msgid "Success!" msgstr "Succès !" -#: sith/settings.py:682 +#: sith/settings.py:684 msgid "Fail!" msgstr "Échec !" -#: sith/settings.py:683 +#: sith/settings.py:685 msgid "You successfully posted an article in the Weekmail" msgstr "Article posté avec succès dans le Weekmail" -#: sith/settings.py:684 +#: sith/settings.py:686 msgid "You successfully edited an article in the Weekmail" msgstr "Article édité avec succès dans le Weekmail" -#: sith/settings.py:685 +#: sith/settings.py:687 msgid "You successfully sent the Weekmail" msgstr "Weekmail envoyé avec succès" -#: sith/settings.py:693 +#: sith/settings.py:695 msgid "AE tee-shirt" msgstr "Tee-shirt AE" @@ -5755,11 +5806,11 @@ msgstr "lieu" 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/stats.jinja:25 +#: subscription/templates/subscription/stats.jinja:27 msgid "Total subscriptions" msgstr "Cotisations totales" -#: subscription/templates/subscription/stats.jinja:26 +#: subscription/templates/subscription/stats.jinja:28 msgid "Subscriptions by type" msgstr "Cotisations par type" @@ -5772,7 +5823,7 @@ msgstr "" msgid "A user with that email address already exists" msgstr "Un utilisateur avec cette adresse email existe déjà" -#: subscription/views.py:101 +#: subscription/views.py:102 msgid "You must either choose an existing user or create a new one properly" msgstr "" "Vous devez soit choisir un utilisateur existant, soit en créer un proprement" @@ -5837,11 +5888,11 @@ msgstr "" "La photo de profil que vous souhaitez voir dans le Trombi (attention: cette " "photo risque d'être publiée)" -#: trombi/models.py:139 +#: trombi/models.py:140 msgid "scrub pict" msgstr "photo de blouse" -#: trombi/models.py:143 +#: trombi/models.py:144 msgid "" "The scrub picture you want in the trombi (warning: this picture may be " "published)" @@ -5849,19 +5900,19 @@ msgstr "" "La photo de blouse que vous souhaitez voir dans le Trombi (attention: cette " "photo risque d'être publiée)" -#: trombi/models.py:185 +#: trombi/models.py:184 msgid "target" msgstr "cible" -#: trombi/models.py:190 +#: trombi/models.py:189 msgid "is the comment moderated" msgstr "le commentaire est modéré" -#: trombi/models.py:212 +#: trombi/models.py:211 msgid "start" msgstr "début" -#: trombi/models.py:213 +#: trombi/models.py:212 msgid "end" msgstr "fin" @@ -5992,27 +6043,27 @@ msgstr "" msgid "Edit comment" msgstr "Éditer le commentaire" -#: trombi/views.py:68 +#: trombi/views.py:69 msgid "My profile" msgstr "Mon profil" -#: trombi/views.py:75 +#: trombi/views.py:76 msgid "My pictures" msgstr "Mes photos" -#: trombi/views.py:87 +#: trombi/views.py:91 msgid "Admin tools" msgstr "Admin Trombi" -#: trombi/views.py:213 +#: trombi/views.py:215 msgid "Explain why you rejected the comment" msgstr "Expliquez pourquoi vous refusez le commentaire" -#: trombi/views.py:244 +#: trombi/views.py:246 msgid "Rejected comment" msgstr "Commentaire rejeté" -#: trombi/views.py:246 +#: trombi/views.py:248 #, python-format msgid "" "Your comment to %(target)s on the Trombi \"%(trombi)s\" was rejected for the " @@ -6029,16 +6080,16 @@ msgstr "" "\n" "%(content)s" -#: trombi/views.py:278 +#: trombi/views.py:280 #, python-format msgid "%(name)s (deadline: %(date)s)" msgstr "%(name)s (date limite: %(date)s)" -#: trombi/views.py:288 +#: trombi/views.py:290 msgid "Select trombi" msgstr "Choisir un trombi" -#: trombi/views.py:290 +#: trombi/views.py:292 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 " @@ -6048,19 +6099,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:361 +#: trombi/views.py:363 msgid "Personal email (not UTBM)" msgstr "Email personnel (pas UTBM)" -#: trombi/views.py:362 +#: trombi/views.py:364 msgid "Phone" msgstr "Téléphone" -#: trombi/views.py:363 +#: trombi/views.py:365 msgid "Native town" msgstr "Ville d'origine" -#: trombi/views.py:471 +#: trombi/views.py:473 msgid "" "You can not yet write comment, you must wait for the subscription deadline " "to be passed." @@ -6068,11 +6119,11 @@ msgstr "" "Vous ne pouvez pas encore écrire de commentaires, vous devez attendre la fin " "des inscriptions" -#: trombi/views.py:478 +#: trombi/views.py:480 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:491 +#: trombi/views.py:493 #, python-format msgid "Maximum characters: %(max_length)s" msgstr "Nombre de caractères max: %(max_length)s" diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 02032e52..644f4a43 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-10-09 11:50+0200\n" +"POT-Creation-Date: 2024-10-16 02:19+0200\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n" "Last-Translator: Sli \n" "Language-Team: AE info \n" @@ -22,87 +22,95 @@ msgstr "" msgid "captured.%s" msgstr "capture.%s" -#: core/static/webpack/easymde-index.js:32 +#: core/static/webpack/ajax-select-index.ts:73 +msgid "You need to type %(number)s more characters" +msgstr "Vous devez taper %(number)s caractères de plus" + +#: core/static/webpack/ajax-select-index.ts:76 +msgid "No results found" +msgstr "Aucun résultat trouvé" + +#: core/static/webpack/easymde-index.ts:31 msgid "Heading" msgstr "Titre" -#: core/static/webpack/easymde-index.js:38 +#: core/static/webpack/easymde-index.ts:37 msgid "Italic" msgstr "Italique" -#: core/static/webpack/easymde-index.js:44 +#: core/static/webpack/easymde-index.ts:43 msgid "Bold" msgstr "Gras" -#: core/static/webpack/easymde-index.js:50 +#: core/static/webpack/easymde-index.ts:49 msgid "Strikethrough" msgstr "Barré" -#: core/static/webpack/easymde-index.js:59 +#: core/static/webpack/easymde-index.ts:58 msgid "Underline" msgstr "Souligné" -#: core/static/webpack/easymde-index.js:68 +#: core/static/webpack/easymde-index.ts:67 msgid "Superscript" msgstr "Exposant" -#: core/static/webpack/easymde-index.js:77 +#: core/static/webpack/easymde-index.ts:76 msgid "Subscript" msgstr "Indice" -#: core/static/webpack/easymde-index.js:83 +#: core/static/webpack/easymde-index.ts:82 msgid "Code" msgstr "Code" -#: core/static/webpack/easymde-index.js:90 +#: core/static/webpack/easymde-index.ts:89 msgid "Quote" msgstr "Citation" -#: core/static/webpack/easymde-index.js:96 +#: core/static/webpack/easymde-index.ts:95 msgid "Unordered list" msgstr "Liste non ordonnée" -#: core/static/webpack/easymde-index.js:102 +#: core/static/webpack/easymde-index.ts:101 msgid "Ordered list" msgstr "Liste ordonnée" -#: core/static/webpack/easymde-index.js:109 +#: core/static/webpack/easymde-index.ts:108 msgid "Insert link" msgstr "Insérer lien" -#: core/static/webpack/easymde-index.js:115 +#: core/static/webpack/easymde-index.ts:114 msgid "Insert image" msgstr "Insérer image" -#: core/static/webpack/easymde-index.js:121 +#: core/static/webpack/easymde-index.ts:120 msgid "Insert table" msgstr "Insérer tableau" -#: core/static/webpack/easymde-index.js:128 +#: core/static/webpack/easymde-index.ts:127 msgid "Clean block" msgstr "Nettoyer bloc" -#: core/static/webpack/easymde-index.js:135 +#: core/static/webpack/easymde-index.ts:134 msgid "Toggle preview" msgstr "Activer la prévisualisation" -#: core/static/webpack/easymde-index.js:141 +#: core/static/webpack/easymde-index.ts:140 msgid "Toggle side by side" msgstr "Activer la vue côte à côte" -#: core/static/webpack/easymde-index.js:147 +#: core/static/webpack/easymde-index.ts:146 msgid "Toggle fullscreen" msgstr "Activer le plein écran" -#: core/static/webpack/easymde-index.js:154 +#: core/static/webpack/easymde-index.ts:153 msgid "Markdown guide" msgstr "Guide markdown" -#: core/static/webpack/user/family-graph-index.js:222 +#: core/static/webpack/user/family-graph-index.js:233 msgid "family_tree.%(extension)s" msgstr "arbre_genealogique.%(extension)s" -#: core/static/webpack/user/pictures-index.js:67 +#: core/static/webpack/user/pictures-index.js:76 msgid "pictures.%(extension)s" msgstr "photos.%(extension)s" @@ -110,10 +118,10 @@ msgstr "photos.%(extension)s" msgid "Incorrect value" msgstr "Valeur incorrecte" -#: sas/static/sas/js/viewer.js:205 +#: sas/static/webpack/sas/viewer-index.ts:271 msgid "Couldn't moderate picture" msgstr "Il n'a pas été possible de modérer l'image" -#: sas/static/sas/js/viewer.js:217 +#: sas/static/webpack/sas/viewer-index.ts:284 msgid "Couldn't delete picture" msgstr "Il n'a pas été possible de supprimer l'image" diff --git a/matmat/urls.py b/matmat/urls.py index e24d933c..dcb5bff9 100644 --- a/matmat/urls.py +++ b/matmat/urls.py @@ -23,7 +23,12 @@ from django.urls import path -from matmat.views import * +from matmat.views import ( + SearchClearFormView, + SearchNormalFormView, + SearchQuickFormView, + SearchReverseFormView, +) urlpatterns = [ path("", SearchNormalFormView.as_view(), name="search"), diff --git a/matmat/views.py b/matmat/views.py index 56fb3330..47840c2d 100644 --- a/matmat/views.py +++ b/matmat/views.py @@ -71,15 +71,15 @@ class SearchForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - for key in self.fields.keys(): + for key in self.fields: self.fields[key].required = False @property def cleaned_data_json(self): data = self.cleaned_data - for key in data.keys(): - if key in ("date_of_birth", "phone") and data[key] is not None: - data[key] = str(data[key]) + for key, val in data.items(): + if key in ("date_of_birth", "phone") and val is not None: + data[key] = str(val) return data @@ -98,10 +98,7 @@ class SearchFormListView(FormerSubscriberMixin, SingleObjectMixin, ListView): self.session = request.session self.last_search = self.session.get("matmat_search_result", str([])) self.last_search = literal_eval(self.last_search) - if "valid_form" in kwargs.keys(): - self.valid_form = kwargs["valid_form"] - else: - self.valid_form = None + self.valid_form = kwargs.get("valid_form") self.init_query = self.model.objects self.can_see_hidden = True @@ -202,8 +199,8 @@ class SearchClearFormView(FormerSubscriberMixin, View): def dispatch(self, request, *args, **kwargs): super().dispatch(request, *args, **kwargs) - if "matmat_search_form" in request.session.keys(): + if "matmat_search_form" in request.session: request.session.pop("matmat_search_form") - if "matmat_search_result" in request.session.keys(): + if "matmat_search_result" in request.session: request.session.pop("matmat_search_result") return HttpResponseRedirect(reverse("matmat:search")) diff --git a/package-lock.json b/package-lock.json index 4bdc0e51..d9b81d6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,9 +28,9 @@ "jquery-ui": "^1.14.0", "jquery.shorten": "^1.0.0", "native-file-system-adapter": "^3.0.1", - "select2": "^4.1.0-rc.0", "three": "^0.169.0", - "three-spritetext": "^1.9.0" + "three-spritetext": "^1.9.0", + "tom-select": "^2.3.1" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -39,7 +39,6 @@ "@hey-api/openapi-ts": "^0.53.8", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "@types/select2": "^4.0.63", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", @@ -2112,6 +2111,19 @@ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", "license": "MIT" }, + "node_modules/@orchidjs/sifter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@orchidjs/sifter/-/sifter-1.0.3.tgz", + "integrity": "sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==", + "dependencies": { + "@orchidjs/unicode-variants": "^1.0.4" + } + }, + "node_modules/@orchidjs/unicode-variants": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz", + "integrity": "sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ==" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6134,6 +6146,7 @@ "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==", "license": "MIT" }, + "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -6617,6 +6630,22 @@ "node": ">=8.0" } }, + "node_modules/tom-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tom-select/-/tom-select-2.3.1.tgz", + "integrity": "sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==", + "dependencies": { + "@orchidjs/sifter": "^1.0.3", + "@orchidjs/unicode-variants": "^1.0.4" + }, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tom-select" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", diff --git a/package.json b/package.json index 189d9a30..fb15fbf4 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@hey-api/openapi-ts": "^0.53.8", "@types/alpinejs": "^3.13.10", "@types/jquery": "^3.5.31", - "@types/select2": "^4.0.63", "babel-loader": "^9.2.1", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", @@ -61,8 +60,8 @@ "jquery-ui": "^1.14.0", "jquery.shorten": "^1.0.0", "native-file-system-adapter": "^3.0.1", - "select2": "^4.1.0-rc.0", "three": "^0.169.0", - "three-spritetext": "^1.9.0" + "three-spritetext": "^1.9.0", + "tom-select": "^2.3.1" } } diff --git a/pedagogy/migrations/0001_initial.py b/pedagogy/migrations/0001_initial.py index 62f9d5c8..2afd9cc7 100644 --- a/pedagogy/migrations/0001_initial.py +++ b/pedagogy/migrations/0001_initial.py @@ -32,7 +32,10 @@ class Migration(migrations.Migration): unique=True, validators=[ django.core.validators.RegexValidator( - message="The code of an UV must only contains uppercase characters without accent and numbers", + message=( + "The code of an UV must only contains " + "uppercase characters without accent and numbers" + ), regex="([A-Z0-9]+)", ) ], diff --git a/pedagogy/models.py b/pedagogy/models.py index 92cd16d5..956e6791 100644 --- a/pedagogy/models.py +++ b/pedagogy/models.py @@ -45,7 +45,8 @@ class UV(models.Model): validators.RegexValidator( regex="([A-Z0-9]+)", message=_( - "The code of an UV must only contains uppercase characters without accent and numbers" + "The code of an UV must only contains " + "uppercase characters without accent and numbers" ), ) ], diff --git a/pedagogy/tests/test_api.py b/pedagogy/tests/test_api.py index f0667bd5..b8fb90b4 100644 --- a/pedagogy/tests/test_api.py +++ b/pedagogy/tests/test_api.py @@ -27,7 +27,10 @@ class TestUVSearch(TestCase): semester="AUTUMN", department="GI", manager="francky", - title="Programmation Orientée Objet: Concepts fondamentaux et mise en pratique avec le langage C++", + title=( + "Programmation Orientée Objet: " + "Concepts fondamentaux et mise en pratique avec le langage C++" + ), ), uv_recipe.prepare( code="MT01", @@ -118,7 +121,7 @@ class TestUVSearch(TestCase): ("M", {"MT01", "MT10"}), ("mt", {"MT01", "MT10"}), ("MT", {"MT01", "MT10"}), - ("algèbre", {"MT01"}), # Title search case insensitive + ("algèbre", {"MT01"}), # Title search case insensitive # Manager search ("moss", {"TNEV"}), ("francky", {"DA50", "AP4A"}), diff --git a/pedagogy/tests/tests.py b/pedagogy/tests/tests.py index 6a59d4c5..cc36f3c3 100644 --- a/pedagogy/tests/tests.py +++ b/pedagogy/tests/tests.py @@ -381,7 +381,9 @@ class TestUVCommentCreationAndDisplay(TestCase): self.assertContains( response, _( - "You already posted a comment on this UV. If you want to comment again, please modify or delete your previous comment." + "You already posted a comment on this UV. " + "If you want to comment again, " + "please modify or delete your previous comment." ), ) diff --git a/pedagogy/urls.py b/pedagogy/urls.py index 7540c27c..bd5e89a6 100644 --- a/pedagogy/urls.py +++ b/pedagogy/urls.py @@ -23,7 +23,17 @@ from django.urls import path -from pedagogy.views import * +from pedagogy.views import ( + UVCommentDeleteView, + UVCommentReportCreateView, + UVCommentUpdateView, + UVCreateView, + UVDeleteView, + UVDetailFormView, + UVGuideView, + UVModerationFormView, + UVUpdateView, +) urlpatterns = [ # Urls displaying the actual application for visitors diff --git a/pedagogy/views.py b/pedagogy/views.py index 6e3a2707..ca2c712e 100644 --- a/pedagogy/views.py +++ b/pedagogy/views.py @@ -23,7 +23,7 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 from django.urls import reverse, reverse_lazy from django.views.generic import ( @@ -193,18 +193,12 @@ class UVModerationFormView(FormView): def form_valid(self, form): form_clean = form.clean() - for report in form_clean.get("accepted_reports", []): - try: - report.comment.delete() # Delete the related comment - except ObjectDoesNotExist: - # To avoid errors when two reports points the same comment - pass - for report in form_clean.get("denied_reports", []): - try: - report.delete() # Delete the report itself - except ObjectDoesNotExist: - # To avoid errors when two reports points the same comment - pass + accepted = form_clean.get("accepted_reports", []) + if len(accepted) > 0: # delete the reported comments + UVComment.objects.filter(reports__in=accepted).delete() + denied = form_clean.get("denied_reports", []) + if len(denied) > 0: # delete the comments themselves + UVCommentReport.objects.filter(id__in={d.id for d in denied}).delete() return super().form_valid(form) def get_success_url(self): diff --git a/poetry.lock b/poetry.lock index 9160dba8..c8c6f856 100644 --- a/poetry.lock +++ b/poetry.lock @@ -343,73 +343,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.2" +version = "7.6.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, - {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, - {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, - {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, - {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, - {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, - {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, - {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, - {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, - {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, - {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, - {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, - {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, - {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, - {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, - {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, - {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, - {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, - {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, - {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, - {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, - {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, - {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, - {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, - {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, - {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, - {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, - {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, - {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, - {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, - {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, - {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, - {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, + {file = "coverage-7.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976"}, + {file = "coverage-7.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c"}, + {file = "coverage-7.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc"}, + {file = "coverage-7.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e"}, + {file = "coverage-7.6.3-cp310-cp310-win32.whl", hash = "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007"}, + {file = "coverage-7.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd"}, + {file = "coverage-7.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b"}, + {file = "coverage-7.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549"}, + {file = "coverage-7.6.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b"}, + {file = "coverage-7.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f"}, + {file = "coverage-7.6.3-cp311-cp311-win32.whl", hash = "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97"}, + {file = "coverage-7.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6"}, + {file = "coverage-7.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6"}, + {file = "coverage-7.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f"}, + {file = "coverage-7.6.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83"}, + {file = "coverage-7.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167"}, + {file = "coverage-7.6.3-cp312-cp312-win32.whl", hash = "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd"}, + {file = "coverage-7.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6"}, + {file = "coverage-7.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6"}, + {file = "coverage-7.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4"}, + {file = "coverage-7.6.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b"}, + {file = "coverage-7.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4"}, + {file = "coverage-7.6.3-cp313-cp313-win32.whl", hash = "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f"}, + {file = "coverage-7.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce"}, + {file = "coverage-7.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3"}, + {file = "coverage-7.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38"}, + {file = "coverage-7.6.3-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5"}, + {file = "coverage-7.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91"}, + {file = "coverage-7.6.3-cp313-cp313t-win32.whl", hash = "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43"}, + {file = "coverage-7.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0"}, + {file = "coverage-7.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2"}, + {file = "coverage-7.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40"}, + {file = "coverage-7.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb"}, + {file = "coverage-7.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13"}, + {file = "coverage-7.6.3-cp39-cp39-win32.whl", hash = "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3"}, + {file = "coverage-7.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d"}, + {file = "coverage-7.6.3-pp39.pp310-none-any.whl", hash = "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181"}, + {file = "coverage-7.6.3.tar.gz", hash = "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054"}, ] [package.extras] @@ -491,13 +491,13 @@ tests = ["noseofyeti[black] (==2.4.9)", "pytest (==8.3.2)"] [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] @@ -814,13 +814,13 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "1.3.2" +version = "1.4.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "griffe-1.3.2-py3-none-any.whl", hash = "sha256:2e34b5e46507d615915c8e6288bb1a2234bd35dee44d01e40a2bc2f25bd4d10c"}, - {file = "griffe-1.3.2.tar.gz", hash = "sha256:1ec50335aa507ed2445f2dd45a15c9fa3a45f52c9527e880571dfc61912fd60c"}, + {file = "griffe-1.4.1-py3-none-any.whl", hash = "sha256:84295ee0b27743bd880aea75632830ef02ded65d16124025e4c263bb826ab645"}, + {file = "griffe-1.4.1.tar.gz", hash = "sha256:911a201b01dc92e08c0e84c38a301e9da5ec067f00e7d9f2e39bc24dbfa3c176"}, ] [package.dependencies] @@ -1283,13 +1283,13 @@ cache = ["platformdirs"] [[package]] name = "mkdocs-material" -version = "9.5.39" +version = "9.5.40" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"}, - {file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"}, + {file = "mkdocs_material-9.5.40-py3-none-any.whl", hash = "sha256:8e7a16ada34e79a7b6459ff2602584222f522c738b6a023d1bea853d5049da6f"}, + {file = "mkdocs_material-9.5.40.tar.gz", hash = "sha256:b69d70e667ec51fc41f65e006a3184dd00d95b2439d982cb1586e4c018943156"}, ] [package.dependencies] @@ -1323,13 +1323,13 @@ files = [ [[package]] name = "mkdocstrings" -version = "0.26.1" +version = "0.26.2" description = "Automatic documentation from sources, for MkDocs." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf"}, - {file = "mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33"}, + {file = "mkdocstrings-0.26.2-py3-none-any.whl", hash = "sha256:1248f3228464f3b8d1a15bd91249ce1701fe3104ac517a5f167a0e01ca850ba5"}, + {file = "mkdocstrings-0.26.2.tar.gz", hash = "sha256:34a8b50f1e6cfd29546c6c09fbe02154adfb0b361bb758834bf56aa284ba876e"}, ] [package.dependencies] @@ -1349,13 +1349,13 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "1.11.1" +version = "1.12.1" description = "A Python handler for mkdocstrings." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af"}, - {file = "mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322"}, + {file = "mkdocstrings_python-1.12.1-py3-none-any.whl", hash = "sha256:205244488199c9aa2a39787ad6a0c862d39b74078ea9aa2be817bc972399563f"}, + {file = "mkdocstrings_python-1.12.1.tar.gz", hash = "sha256:60d6a5ca912c9af4ad431db6d0111ce9f79c6c48d33377dde6a05a8f5f48d792"}, ] [package.dependencies] @@ -1365,13 +1365,13 @@ mkdocstrings = ">=0.26" [[package]] name = "model-bakery" -version = "1.19.5" +version = "1.20.0" description = "Smart object creation facility for Django." optional = false python-versions = ">=3.8" files = [ - {file = "model_bakery-1.19.5-py3-none-any.whl", hash = "sha256:09ecbbf124d32614339581b642c82ac4a73147442f598c7bad23eece24187e5c"}, - {file = "model_bakery-1.19.5.tar.gz", hash = "sha256:37cece544a33f8899ed8f0488cd6a9d2b0b6925e7b478a4ff2786dece8c63745"}, + {file = "model_bakery-1.20.0-py3-none-any.whl", hash = "sha256:875326466f5982ee8f0281abdfa774d78893d5473562575dfd5a9304ac7c5b8c"}, + {file = "model_bakery-1.20.0.tar.gz", hash = "sha256:ec9dc846b9a00b20f92df38fac310263323ab61b59b6eeebf77a4aefb0412724"}, ] [package.dependencies] @@ -1599,13 +1599,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, + {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, ] [package.dependencies] @@ -2192,58 +2192,70 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rjsmin" -version = "1.2.2" +version = "1.2.3" description = "Javascript Minifier" optional = false python-versions = "*" files = [ - {file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4420107304ba7a00b5b9b56cdcd166b9876b34e626829fc4552c85d8fdc3737a"}, - {file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:155a2f3312c1f8c6cec7b5080581cafc761dc0e41d64bfb5d46a772c5230ded8"}, - {file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:88fcb58d65f88cbfa752d51c1ebe5845553f9706def6d9671e98283411575e3e"}, - {file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6eae13608b88f4ce32e0557c8fdef58e69bb4d293182202a03e800f0d33b5268"}, - {file = "rjsmin-1.2.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:81f92fb855fb613ebd04a6d6d46483e71fe3c4f22042dc30dcc938fbd748e59c"}, - {file = "rjsmin-1.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:897db9bf25538047e9388951d532dc291a629b5d041180a8a1a8c102e9d44b90"}, - {file = "rjsmin-1.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5938af8c46734f92f74fdc4d0b6324137c0e09f0a8c3825c83e4cfca1b532e40"}, - {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0424a7b9096fa2b0ab577c4dc7acd683e6cfb5c718ad39a9fb293cb6cbaba95b"}, - {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1714ed93c2bd40c5f970905d2eeda4a6844e09087ae11277d4d43b3e68c32a47"}, - {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:35596fa6d2d44a5471715c464657123995da78aa6f79bccfbb4b8d6ff7d0a4b4"}, - {file = "rjsmin-1.2.2-cp311-cp311-manylinux1_i686.whl", hash = "sha256:3968667158948355b9a62e9641497aac7ac069c076a595e93199d0fe3a40217a"}, - {file = "rjsmin-1.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d07d14354694f6a47f572f2aa2a1ad74b76723e62a0d2b6df796138b71888247"}, - {file = "rjsmin-1.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a78dfa6009235b902454ac53264252b7b94f1e43e3a9e97c4cadae88e409b882"}, - {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9b7a45001e58243a455d11d2de925cadb8c2a0dc737001de646a0f4d90cf0034"}, - {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:86c5e657b74b6c9482bb96f18a79d61750f4e8204759cce179f7eb17d395c683"}, - {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c30b86c7232443a4a726e1bbee34f800556e581e95fc07194ecbf8e02d1d2"}, - {file = "rjsmin-1.2.2-cp312-cp312-manylinux1_i686.whl", hash = "sha256:8982c3ef27fac26dd6b7d0c55ae98fa550fee72da2db010b87211e4b5dd78a67"}, - {file = "rjsmin-1.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:3fc27ae4ece99e2c994cd79df2f0d3f7ac650249f632d19aa8ce85118e33bf0f"}, - {file = "rjsmin-1.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:41113d8d6cae7f7406b30143cc49cc045bbb3fadc2f28df398cea30e1daa60b1"}, - {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3aa09a89b2b7aa2b9251329fe0c3e36c2dc2f10f78b8811e5be92a072596348b"}, - {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5abb8d1241f4ea97950b872fa97a422ba8413fe02358f64128ff0cf745017f07"}, - {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5abc686a9ef7eaf208f9ad1fb5fb949556ecb7cc1fee27290eb7f194e01d97bd"}, - {file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:076adcf04c34f712c9427fd9ba6a75bbf7aab975650dfc78cbdd0fbdbe49ca63"}, - {file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8cb8947ddd250fce58261b0357846cd5d55419419c0f7dfb131dc4b733579a26"}, - {file = "rjsmin-1.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9069c48b6508b9c5b05435e2c6042c2a0e2f97b35d7b9c27ceaea5fd377ffdc5"}, - {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:02b61cf9b6bc518fdac667f3ca3dab051cb8bd1bf4cba28b6d29153ec27990ad"}, - {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:09eca8581797244587916e5e07e36c4c86d54a4b7e5c7697484a95b75803515d"}, - {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c52b9dd45c837f1c5c2e8d40776f9e63257f8dbd5f79b85f648cc70da6c1e4e9"}, - {file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4fe4ce990412c053a6bcd47d55133927e22fd3d100233d73355f60f9053054c5"}, - {file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa883b9363b5134239066060879d5eb422a0d4ccf24ccf871f65a5b34c64926f"}, - {file = "rjsmin-1.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6f4e95c5ac95b4cbb519917b3aa1d3d92fc6939c371637674c4a42b67b2b3f44"}, - {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ae3cd64e18e62aa330b24dd6f7b9809ce0a694afd1f01fe99c21f9acd1cb0ea6"}, - {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7999d797fcf805844d2d91598651785497249f592f31674da0964e794b3be019"}, - {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e733fea039a7b5ad7c06cc8bf215ee7afac81d462e273b3ab55c1ccc906cf127"}, - {file = "rjsmin-1.2.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ccca74461bd53a99ff3304fcf299ea861df89846be3207329cb82d717ce47ea6"}, - {file = "rjsmin-1.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:88f59ad24f91bf9c25d5c2ca3c84a72eed0028f57a98e3b85a915ece5c25be1e"}, - {file = "rjsmin-1.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7a8b56fbd64adcc4402637f0e07b90b441e9981d720a10eb6265118018b42682"}, - {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c24686cfdf86e55692183f7867e72c9e982add479c244eda7b8390f96db2c6c"}, - {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c0d9f9ea8d9cd48cbcdc74a1c2e85d4d588af12bb8f0b672070ae7c9b6e6306"}, - {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:27abd32c9f5b6e0c0a3bcad43e8e24108c6d6c13a4e6c50c97497ea2b4614bb4"}, - {file = "rjsmin-1.2.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:e0e009f6f8460901f5144b34ac2948f94af2f9b8c9b5425da705dbc8152c36c2"}, - {file = "rjsmin-1.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:41e6013cb37a5b3563c19aa35f8e659fa536aa4197a0e3b6a57a381638294a15"}, - {file = "rjsmin-1.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62cbd38c9f5090f0a6378a45c415b4f96ae871216cedab0dfa21965620c0be4c"}, - {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fd5254d36f10a17564b63e8bf9ac579c7b5f211364e11e9753ff5b562843c67"}, - {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6cf0309d001a0d45d731dbaab1afd0c23d135c9e029fe56c935c1798094686fc"}, - {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfbe333dab8d23f0a71da90e2d8e8b762a739cbd55a6f948b2dfda089b6d5853"}, - {file = "rjsmin-1.2.2.tar.gz", hash = "sha256:8c1bcd821143fecf23242012b55e13610840a839cd467b358f16359010d62dae"}, + {file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:86e4257875d89b0f7968af9e7c0292e72454f6c75031d1818997782b2e8425a8"}, + {file = "rjsmin-1.2.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2ab72093591127e627b13c1243d4fef40c10593c733517999682f7f2ebf47ee"}, + {file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:448b9eb9fd7b6a70beb5c728a41bc23561dd011f0b8fcf7ed9855b6be198c9a2"}, + {file = "rjsmin-1.2.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebd3948f9bc912525bab93f61c694b11410296b5fd0806e988d42378ef302b8e"}, + {file = "rjsmin-1.2.3-cp310-cp310-manylinux1_i686.whl", hash = "sha256:823f856b40681328157e5dffc0a588dddefb4b6ce49f79de994dfca6084617be"}, + {file = "rjsmin-1.2.3-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:15e3019f0823a003741ddb93e0c70c5d22567acd0757a7edacc40face1517029"}, + {file = "rjsmin-1.2.3-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:dece04e309e29879c12dca8af166ea5d77c497ec932cf82e4a1eb24d1489c398"}, + {file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dd4a1e527568c3a9711ff1d5251763645c14df02d52a45aec089836600b664ea"}, + {file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:78aaa3b79a244a4e21164ce355ce22a5a0d7f2d7841a10343009406a3d34d9bb"}, + {file = "rjsmin-1.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ea4617618cbf78d98756878a292309f6f54fb4ea1b1ea406f79e88eda4d5d50"}, + {file = "rjsmin-1.2.3-cp311-cp311-manylinux1_i686.whl", hash = "sha256:85957171184ef2dee1957cef5e4adb93a7e2702c12c30bd74420ebace1756e89"}, + {file = "rjsmin-1.2.3-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:b6485014e9cbec9a41fb4a7b96ce511ab45a5db8c54ca57ad610f53747e7bab1"}, + {file = "rjsmin-1.2.3-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:64ac6ef8753c56179a53e237ea4d2b3ccdef88b8b51141618311d48e31013207"}, + {file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dbd5f653b5ebcd4920793009ffa210ad5523c523e39e45ee1a0770e4323126dc"}, + {file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b0174d7786dcebab808485d1c27f049c74b97590cddcd62f6ed54796a2c6503b"}, + {file = "rjsmin-1.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6cf24720ea214cbffa0ed96ba0dc24a5cf3ff3cbf59d44a1018292424b48452a"}, + {file = "rjsmin-1.2.3-cp312-cp312-manylinux1_i686.whl", hash = "sha256:ac911d1a12a6d7879ba52e08c56b0ad1a74377bae52610ea74f0f9d936d41785"}, + {file = "rjsmin-1.2.3-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:57a0b2f13402623e4ec44eb7ad8846387b2d5605aa8732a05ebefb2289c24b96"}, + {file = "rjsmin-1.2.3-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:e28610cca3ab03e43113eadad4f7dd9ea235ddc29a8dc5462bb161a80e5d251f"}, + {file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d4afb4fc3624dc44a7fbae4e41c0b5dc5d861a7f5de865ad463041ec1b5d835c"}, + {file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ca26b80c7e63cf0788b41571a4bd08d175df7719364e0dd9a3cf7b6cb1ab834c"}, + {file = "rjsmin-1.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fcc22001588b84d34bbf2c77afa519894244150c4b0754a6e573298ffac24666"}, + {file = "rjsmin-1.2.3-cp313-cp313-manylinux1_i686.whl", hash = "sha256:624d1a0a35122f3f8955d160a39305cf6f786a5b346ee34c516b391cb153a106"}, + {file = "rjsmin-1.2.3-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:72bd04b7db6190339d8214a5fd289ca31fc1ed30a240f8b0ca13acb9ce3a88af"}, + {file = "rjsmin-1.2.3-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:7559f59f4749519b92d72bb68e33b68463f479a82a2a739f1b28a853067aa0e7"}, + {file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:aa8bdecf278f754d1a133ab51119a63a4d38500557912bb0930ae0fd61437ec6"}, + {file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:2078acc2d02a005ef122eb330e941462c8c3102cf798ad49f1c5ec18ac714240"}, + {file = "rjsmin-1.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fa40584fddb4f1d2236119505f6c2fe2b57a1ebaf6eaee2bb2eaac33d2a4ca73"}, + {file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_i686.whl", hash = "sha256:bbe5d8340878b38dd4f7b879ed7728f6fc3d7524ad81a5cfbe4eb8ae63951407"}, + {file = "rjsmin-1.2.3-cp313-cp313t-manylinux1_x86_64.whl", hash = "sha256:c298c93f5633cf894325907cf49fc7fb010c0f75dc9cda90b0fc1684ad19e5a3"}, + {file = "rjsmin-1.2.3-cp313-cp313t-manylinux2014_aarch64.whl", hash = "sha256:35f18cffe3f1bf6d96bcfd977199378ebfd641d823b08e235d1e0bb0fbaa5532"}, + {file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9aeadf4dd5f941bebf110fe83960a4bafdac176647537819bb7662f5e9a37aaa"}, + {file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c3219e6e22897b31c8598cb412ed56bc12a722c1d4f88a71710c16efe8c07d0c"}, + {file = "rjsmin-1.2.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:bceccb06b118be890fe735fc09ee256851f4993708cb3647f6c71dd0151cce89"}, + {file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f3620271f00b8ba3c7c5134ca1d99cde5fd1bf1e84aa96aa65c177ee634122f7"}, + {file = "rjsmin-1.2.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f3d86f70fcca5f68b65eabbce365d07d80404ecd6aa9c55ba9e9f1042a3514c7"}, + {file = "rjsmin-1.2.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1dae9230eae6d7eb2820a511cb640ca6f2e5b91ff78805d71332e8a65a898ea1"}, + {file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b788c3ec9d68d8fda2240eb7831bdfb2cc0c88d5fb38c9ed6e0fd090eb5d1490"}, + {file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4763efbfad7fbf3240a33f08f64991bf0db07453caf283eea51ade84053e9bb7"}, + {file = "rjsmin-1.2.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e1379e448da75e2426205c756e79d7b9ba1b7ed616fe97122d72c3fe054e8cac"}, + {file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:27e134f4d91a5986cba6dced5cb539947a3ec61544ab5ef31b74b384ddc03931"}, + {file = "rjsmin-1.2.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2674fcad70d0fab4c1c71e4ac1d4d67935f67e6ecc3924de0dd1264c80a9f9a2"}, + {file = "rjsmin-1.2.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b1f2540bd0ce7eda326df7b3bfa360f6edd526bfcb959b5d136afdbccddf0765"}, + {file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:23f3b4adde995a0d0b7835558840dd4673adf99d2473b6d40474d30801d6c57b"}, + {file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5fb0574eb541d374a2751e9c0ae019fdd86c9e3eb2e7cf893756886e7b3923f"}, + {file = "rjsmin-1.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18d6a3229d1ed511a0b0a9a7271ef58ff3b02ba408b92b426857b33b137e7f15"}, + {file = "rjsmin-1.2.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7fe1181452fca7713f377cb6c43cd139638a9edc8c8c29c67119626df164b317"}, + {file = "rjsmin-1.2.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:57708a4e637aac571c578424a7092d3ec64afb1eabbb73e0c71659457eac9ee4"}, + {file = "rjsmin-1.2.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:4c1b5a888d43063a22e2e2c2b4db4d6139dfa6e0d2903ae9bb050ed63a340f40"}, + {file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e4ac3f85df88d636a9680432fbbf5d3fe1f171821688106a6710738f06575fc2"}, + {file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9dff6b14f92ca7a9f6fbf13548358715e47c5e69576aa5dd8b0ad5048fdc967f"}, + {file = "rjsmin-1.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07c4f1efbbbcd16a645ada1f012595f3eb3e5d5933395effb6104d3731de2d96"}, + {file = "rjsmin-1.2.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:37a73f6ff49dd8c662399575a249a2a028d098c1fa940c6e88aa9082beb44eca"}, + {file = "rjsmin-1.2.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:73357ec91465cf69173d637ccde7b46ed3a8001161c9650325fa305a486e89a3"}, + {file = "rjsmin-1.2.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:236c792fbe18c3b18d4e0ad5ff1b1145f1fbe02126aee9f21bca757b00b63b7e"}, + {file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a630a3131a4e63e10665a0ea7cfe0784a3e1e1c854edf79a8ac0654e3756648"}, + {file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a1c98f60ca57adbae023cf989eec91d052f0601df63ddc52a0a48303b21a7f9e"}, + {file = "rjsmin-1.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:32a0174efac83ac72a681edcb9acf5e1c87c5b6aae65ed3424468b5945a90f9d"}, + {file = "rjsmin-1.2.3.tar.gz", hash = "sha256:1388b52493a4c04fbc970a2d757c301fa05a3c37640314c2ce9dfc8d8a730cc6"}, ] [[package]] @@ -2679,4 +2691,4 @@ filelock = ">=3.4" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "730b020b335ea67342069c40591f6959f1e5f01eef40806c95777def2f39eb37" +content-hash = "cb47f6409e629d8369a19d82f44a57dbe9414c79e6e72bd88a6bcb34d78f0bc0" diff --git a/pyproject.toml b/pyproject.toml index d8c71146..7dc2e8dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ optional = true # deps used for development purposes, but unneeded in prod django-debug-toolbar = "^4.4.6" ipython = "^8.26.0" -pre-commit = "^3.8.0" +pre-commit = "^4.0.1" ruff = "^0.6.9" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml djhtml = "^3.0.6" faker = "^30.3.0" @@ -77,14 +77,14 @@ freezegun = "^1.5.1" # used to test time-dependent code pytest = "^8.3.2" pytest-cov = "^5.0.0" pytest-django = "^4.9.0" -model-bakery = "^1.19.5" +model-bakery = "^1.20.0" [tool.poetry.group.docs.dependencies] # deps used to work on the documentation mkdocs = "^1.6.1" -mkdocs-material = "^9.5.39" -mkdocstrings = "^0.26.1" -mkdocstrings-python = "^1.11.1" +mkdocs-material = "^9.5.40" +mkdocstrings = "^0.26.2" +mkdocstrings-python = "^1.12.0" mkdocs-include-markdown-plugin = "^6.2.2" [tool.poetry.group.docs] @@ -101,17 +101,30 @@ select = [ "A", # shadowing of Python builtins "B", "C4", # use comprehensions when possible - "I", # isort "DJ", # django-specific rules, - "F401", # unused import + "E", # pycodestyle (https://docs.astral.sh/ruff/rules/#pycodestyle-e-w) + "ERA", # commented code + "F", # pyflakes (https://docs.astral.sh/ruff/rules/#pyflakes-f) "FBT", # boolean trap + "FLY", # f-string instead of str.join + "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb + "I", # isort + "INT", # gettext + "PERF", # performance + "PLW", # pylint warnings (https://docs.astral.sh/ruff/rules/#pylint-pl) + "RUF", # Ruff specific rules + "SIM", # simplify (https://docs.astral.sh/ruff/rules/#flake8-simplify-sim) + "T100", # breakpoint() + "T2", # print statements + "TCH", # type-checking block "UP008", # Use super() instead of super(__class__, self) "UP009", # utf-8 encoding declaration is unnecessary - "T2", # print statements ] ignore = [ "DJ001", # null=True in CharField/TextField. this one would require a migration + "E501", # line too long. The rule is too harsh, and the formatter deals with it in most cases + "RUF012" # mutable class attributes. This rule doesn't integrate well with django ] [tool.ruff.lint.pydocstyle] diff --git a/rootplace/management/commands/delete_all_forum_user_messages.py b/rootplace/management/commands/delete_all_forum_user_messages.py index 45153608..576055e5 100644 --- a/rootplace/management/commands/delete_all_forum_user_messages.py +++ b/rootplace/management/commands/delete_all_forum_user_messages.py @@ -44,8 +44,8 @@ class Command(BaseCommand): exit(1) confirm = input( - "User selected: %s\nDo you really want to delete all message from this user ? [y/N] " - % (user,) + "User selected: %s\nDo you really want " + "to delete all message from this user ? [y/N] " % (user,) ) if not confirm.lower().startswith("y"): diff --git a/rootplace/tests.py b/rootplace/tests.py index f5d6fb73..0d0f1542 100644 --- a/rootplace/tests.py +++ b/rootplace/tests.py @@ -66,11 +66,11 @@ class TestMergeUser(TestCase): self.to_keep = User.objects.get(pk=self.to_keep.pk) # fields of to_delete should be assigned to to_keep # if they were not set beforehand - assert "Biggus" == self.to_keep.first_name - assert "Dickus" == self.to_keep.last_name - assert "B'ian" == self.to_keep.nick_name - assert "Jerusalem" == self.to_keep.address - assert "Rome" == self.to_keep.parent_address + assert self.to_keep.first_name == "Biggus" + assert self.to_keep.last_name == "Dickus" + assert self.to_keep.nick_name == "B'ian" + assert self.to_keep.address == "Jerusalem" + assert self.to_keep.parent_address == "Rome" assert self.to_keep.groups.count() == 3 groups = sorted(self.to_keep.groups.all(), key=lambda i: i.id) expected = sorted([subscribers, mde_admin, sas_admin], key=lambda i: i.id) diff --git a/rootplace/urls.py b/rootplace/urls.py index fbf21b97..81568558 100644 --- a/rootplace/urls.py +++ b/rootplace/urls.py @@ -24,7 +24,11 @@ from django.urls import path -from rootplace.views import * +from rootplace.views import ( + DeleteAllForumUserMessagesView, + MergeUsersView, + OperationLogListView, +) urlpatterns = [ path("merge/", MergeUsersView.as_view(), name="merge"), diff --git a/rootplace/views.py b/rootplace/views.py index 12b4fa23..4aefb8c3 100644 --- a/rootplace/views.py +++ b/rootplace/views.py @@ -48,7 +48,8 @@ def __merge_subscriptions(u1: User, u2: User): Some examples : - if u1 is not subscribed, his subscription end date become the one of u2 - if u1 is subscribed but not u2, nothing happen - - if u1 is subscribed for, let's say, 2 remaining months and u2 is subscribed for 3 remaining months, + - if u1 is subscribed for, let's say, + 2 remaining months and u2 is subscribed for 3 remaining months, he shall then be subscribed for 5 months """ last_subscription = ( diff --git a/sas/admin.py b/sas/admin.py index f2845ad3..ac980341 100644 --- a/sas/admin.py +++ b/sas/admin.py @@ -15,7 +15,7 @@ from django.contrib import admin -from sas.models import Album, PeoplePictureRelation, Picture +from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest @admin.register(Picture) @@ -31,4 +31,15 @@ class PeoplePictureRelationAdmin(admin.ModelAdmin): autocomplete_fields = ("picture", "user") -admin.site.register(Album) +@admin.register(Album) +class AlbumAdmin(admin.ModelAdmin): + list_display = ("name", "parent", "date", "owner", "is_moderated") + search_fields = ("name",) + autocomplete_fields = ("owner", "parent", "edit_groups", "view_groups") + + +@admin.register(PictureModerationRequest) +class PictureModerationRequestAdmin(admin.ModelAdmin): + list_display = ("author", "picture", "created_at") + search_fields = ("author", "picture") + autocomplete_fields = ("author", "picture") diff --git a/sas/api.py b/sas/api.py index 32b64cc5..ca4c10c6 100644 --- a/sas/api.py +++ b/sas/api.py @@ -9,10 +9,17 @@ from ninja_extra.permissions import IsAuthenticated from ninja_extra.schemas import PaginatedResponseSchema from pydantic import NonNegativeInt -from core.api_permissions import CanView, IsOwner +from core.api_permissions import CanView, IsInGroup, IsRoot from core.models import Notification, User from sas.models import PeoplePictureRelation, Picture -from sas.schemas import IdentifiedUserSchema, PictureFilterSchema, PictureSchema +from sas.schemas import ( + IdentifiedUserSchema, + ModerationRequestSchema, + PictureFilterSchema, + PictureSchema, +) + +IsSasAdmin = IsRoot | IsInGroup(settings.SITH_GROUP_SAS_ADMIN_ID) @api_controller("/sas/picture") @@ -85,18 +92,35 @@ class PicturesController(ControllerBase): }, ) - @route.delete("/{picture_id}", permissions=[IsOwner]) + @route.delete("/{picture_id}", permissions=[IsSasAdmin]) def delete_picture(self, picture_id: int): self.get_object_or_exception(Picture, pk=picture_id).delete() - @route.patch("/{picture_id}/moderate", permissions=[IsOwner]) + @route.patch( + "/{picture_id}/moderation", + permissions=[IsSasAdmin], + url_name="picture_moderate", + ) def moderate_picture(self, picture_id: int): + """Mark a picture as moderated and remove its pending moderation requests.""" picture = self.get_object_or_exception(Picture, pk=picture_id) + picture.moderation_requests.all().delete() picture.is_moderated = True picture.moderator = self.context.request.user picture.asked_for_removal = False picture.save() + @route.get( + "/{picture_id}/moderation", + permissions=[IsSasAdmin], + response=list[ModerationRequestSchema], + url_name="picture_moderation_requests", + ) + def fetch_moderation_requests(self, picture_id: int): + """Fetch the moderation requests issued on this picture.""" + picture = self.get_object_or_exception(Picture, pk=picture_id) + return picture.moderation_requests.select_related("author") + @api_controller("/sas/relation", tags="User identification on SAS pictures") class UsersIdentifiedController(ControllerBase): diff --git a/sas/forms.py b/sas/forms.py new file mode 100644 index 00000000..6569e92a --- /dev/null +++ b/sas/forms.py @@ -0,0 +1,124 @@ +from typing import Any + +from ajax_select import make_ajax_field +from ajax_select.fields import AutoCompleteSelectMultipleField +from django import forms +from django.utils.translation import gettext_lazy as _ + +from core.models import User +from core.views import MultipleImageField +from core.views.forms import SelectDate +from sas.models import Album, PeoplePictureRelation, Picture, PictureModerationRequest + + +class SASForm(forms.Form): + album_name = forms.CharField( + label=_("Add a new album"), max_length=Album.NAME_MAX_LENGTH, required=False + ) + images = MultipleImageField( + label=_("Upload images"), + required=False, + ) + + def process(self, parent, owner, files, *, automodere=False): + try: + if self.cleaned_data["album_name"] != "": + album = Album( + parent=parent, + name=self.cleaned_data["album_name"], + owner=owner, + is_moderated=automodere, + ) + album.clean() + album.save() + except Exception as e: + self.add_error( + None, + _("Error creating album %(album)s: %(msg)s") + % {"album": self.cleaned_data["album_name"], "msg": repr(e)}, + ) + for f in files: + new_file = Picture( + parent=parent, + name=f.name, + file=f, + owner=owner, + mime_type=f.content_type, + size=f.size, + is_folder=False, + is_moderated=automodere, + ) + if automodere: + new_file.moderator = owner + try: + new_file.clean() + new_file.generate_thumbnails() + new_file.save() + except Exception as e: + self.add_error( + None, + _("Error uploading file %(file_name)s: %(msg)s") + % {"file_name": f, "msg": repr(e)}, + ) + + +class RelationForm(forms.ModelForm): + class Meta: + model = PeoplePictureRelation + fields = ["picture"] + widgets = {"picture": forms.HiddenInput} + + users = AutoCompleteSelectMultipleField( + "users", show_help_text=False, help_text="", label=_("Add user"), required=False + ) + + +class PictureEditForm(forms.ModelForm): + class Meta: + model = Picture + fields = ["name", "parent"] + + parent = make_ajax_field(Picture, "parent", "files", help_text="") + + +class AlbumEditForm(forms.ModelForm): + class Meta: + model = Album + fields = ["name", "date", "file", "parent", "edit_groups"] + + name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name")) + date = forms.DateField(label=_("Date"), widget=SelectDate, required=True) + parent = make_ajax_field(Album, "parent", "files", help_text="") + edit_groups = make_ajax_field(Album, "edit_groups", "groups", help_text="") + recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False) + + +class PictureModerationRequestForm(forms.ModelForm): + """Form to create a PictureModerationRequest. + + The form only manages the reason field, + because the author and the picture are set in the view. + """ + + class Meta: + model = PictureModerationRequest + fields = ["reason"] + + def __init__(self, *args, user: User, picture: Picture, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.picture = picture + + def clean(self) -> dict[str, Any]: + if PictureModerationRequest.objects.filter( + author=self.user, picture=self.picture + ).exists(): + raise forms.ValidationError( + _("You already requested moderation for this picture.") + ) + return super().clean() + + def save(self, *, commit=True) -> PictureModerationRequest: + self.instance.author = self.user + self.instance.picture = self.picture + return super().save(commit) diff --git a/sas/migrations/0004_picturemoderationrequest_and_more.py b/sas/migrations/0004_picturemoderationrequest_and_more.py new file mode 100644 index 00000000..e07b925d --- /dev/null +++ b/sas/migrations/0004_picturemoderationrequest_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.16 on 2024-10-10 20:44 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("sas", "0003_sasfile"), + ] + + operations = [ + migrations.CreateModel( + name="PictureModerationRequest", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "reason", + models.TextField( + default="", + help_text="Why do you want this image to be removed ?", + verbose_name="Reason", + ), + ), + ], + options={ + "verbose_name": "Picture moderation request", + "verbose_name_plural": "Picture moderation requests", + }, + ), + migrations.AddField( + model_name="picturemoderationrequest", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_requests", + to=settings.AUTH_USER_MODEL, + verbose_name="Author", + ), + ), + migrations.AddField( + model_name="picturemoderationrequest", + name="picture", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="moderation_requests", + to="sas.picture", + verbose_name="Picture", + ), + ), + migrations.AddConstraint( + model_name="picturemoderationrequest", + constraint=models.UniqueConstraint( + fields=("author", "picture"), name="one_request_per_user_per_picture" + ), + ), + ] diff --git a/sas/models.py b/sas/models.py index 43f26ccd..bf87786d 100644 --- a/sas/models.py +++ b/sas/models.py @@ -15,6 +15,7 @@ from __future__ import annotations +import contextlib from io import BytesIO from pathlib import Path from typing import ClassVar, Self @@ -108,10 +109,8 @@ class Picture(SasFile): def generate_thumbnails(self, *, overwrite=False): im = Image.open(BytesIO(self.file.read())) - try: + with contextlib.suppress(Exception): im = exif_auto_rotate(im) - except: - pass # convert the compressed image and the thumbnail into webp # The original image keeps its original type, because it's not # meant to be shown on the website, but rather to keep the real image @@ -273,16 +272,12 @@ class PeoplePictureRelation(models.Model): User, verbose_name=_("user"), related_name="pictures", - null=False, - blank=False, on_delete=models.CASCADE, ) picture = models.ForeignKey( Picture, verbose_name=_("picture"), related_name="people", - null=False, - blank=False, on_delete=models.CASCADE, ) @@ -290,4 +285,39 @@ class PeoplePictureRelation(models.Model): unique_together = ["user", "picture"] def __str__(self): - return self.user.get_display_name() + " - " + str(self.picture) + return f"Moderation request by {self.user.get_short_name()} - {self.picture}" + + +class PictureModerationRequest(models.Model): + """A request to remove a Picture from the SAS.""" + + created_at = models.DateTimeField(auto_now_add=True) + author = models.ForeignKey( + User, + verbose_name=_("Author"), + related_name="moderation_requests", + on_delete=models.CASCADE, + ) + picture = models.ForeignKey( + Picture, + verbose_name=_("Picture"), + related_name="moderation_requests", + on_delete=models.CASCADE, + ) + reason = models.TextField( + verbose_name=_("Reason"), + default="", + help_text=_("Why do you want this image to be removed ?"), + ) + + class Meta: + verbose_name = _("Picture moderation request") + verbose_name_plural = _("Picture moderation requests") + constraints = [ + models.UniqueConstraint( + fields=["author", "picture"], name="one_request_per_user_per_picture" + ) + ] + + def __str__(self): + return f"Moderation request by {self.author.get_short_name()}" diff --git a/sas/schemas.py b/sas/schemas.py index 90bbfc90..6647f7d1 100644 --- a/sas/schemas.py +++ b/sas/schemas.py @@ -4,8 +4,8 @@ from django.urls import reverse from ninja import FilterSchema, ModelSchema, Schema from pydantic import Field, NonNegativeInt -from core.schemas import UserProfileSchema -from sas.models import Picture +from core.schemas import SimpleUserSchema, UserProfileSchema +from sas.models import Picture, PictureModerationRequest class PictureFilterSchema(FilterSchema): @@ -52,3 +52,11 @@ class PictureRelationCreationSchema(Schema): class IdentifiedUserSchema(Schema): id: int user: UserProfileSchema + + +class ModerationRequestSchema(ModelSchema): + author: SimpleUserSchema + + class Meta: + model = PictureModerationRequest + fields = ["id", "created_at", "reason"] diff --git a/sas/static/sas/css/picture.scss b/sas/static/sas/css/picture.scss index b8c0f6fb..f62bb8bf 100644 --- a/sas/static/sas/css/picture.scss +++ b/sas/static/sas/css/picture.scss @@ -29,7 +29,7 @@ width: 100%; } - > .photo { + >.photo { box-sizing: border-box; height: 500px; display: flex; @@ -42,7 +42,7 @@ height: auto; } - > img { + >img { height: 100%; max-width: 100%; object-fit: contain; @@ -57,7 +57,7 @@ width: 100%; } - > .navigation { + >.navigation { display: flex; flex-direction: row; gap: 10px; @@ -66,8 +66,8 @@ width: 100%; } - > #prev, - > #next { + >#prev, + >#next { width: calc(50% - 5px); aspect-ratio: 16/9; background: #333333; @@ -80,6 +80,7 @@ object-fit: cover; opacity: 70%; } + .overlay { position: absolute; top: 50%; @@ -89,7 +90,7 @@ font-size: 40px; } - > div { + >div { display: flex; position: relative; width: 100%; @@ -98,12 +99,12 @@ } } - > .tags { + >.tags { @media (min-width: 1001px) { margin-right: 5px; } - > ul { + >ul { list-style-type: none; margin: 0; display: flex; @@ -118,7 +119,7 @@ margin-right: 5px; } - > li { + >li { box-sizing: border-box; display: flex; flex-direction: row; @@ -135,7 +136,7 @@ max-width: calc(50% - 5px); } - > a { + >a { box-sizing: border-box; display: flex; flex-direction: row; @@ -155,7 +156,7 @@ background-color: #aaa; } - > span { + >span { width: 100%; text-overflow: ellipsis; white-space: nowrap; @@ -167,14 +168,14 @@ margin-left: 10px; } - > img { + >img { width: 25px; max-height: 25px; object-fit: contain; border-radius: 50%; } - > .profile-pic { + >.profile-pic { background-position: center center; background-size: cover; background-repeat: no-repeat; @@ -187,23 +188,24 @@ } } - > form { - > p { + >form { + >p { box-sizing: border-box; } - > .results_on_deck > div { + >.results_on_deck>div { position: relative; display: flex; align-items: center; word-break: break-word; - > span { + >span { position: absolute; top: 0; right: 0; } } + input { min-width: 100%; max-width: 100%; @@ -226,17 +228,17 @@ flex-direction: column; } - > .infos { + >.infos { display: flex; flex-direction: column; width: 50%; - > div > div { + >div>div { display: flex; flex-direction: row; justify-content: space-between; - > *:first-child { + >*:first-child { min-width: 150px; @media (max-width: 1000px) { @@ -246,18 +248,18 @@ } } - > .tools { + >.tools { display: flex; flex-direction: column; width: 50%; - > div { + >div { display: flex; flex-direction: row; justify-content: space-between; - > div { - > a.button { + >div { + >a.button { box-sizing: border-box; background-color: #f2f2f2; display: flex; @@ -274,7 +276,7 @@ } } - > a.text.danger { + >a.text.danger { color: red; &:hover { @@ -289,4 +291,4 @@ } } } -} +} \ No newline at end of file diff --git a/sas/static/webpack/sas/viewer-index.ts b/sas/static/webpack/sas/viewer-index.ts index a40e2470..b084810c 100644 --- a/sas/static/webpack/sas/viewer-index.ts +++ b/sas/static/webpack/sas/viewer-index.ts @@ -1,24 +1,20 @@ -import { makeUrl, paginated } from "#core:utils/api"; +import { paginated } from "#core:utils/api"; import { exportToHtml } from "#core:utils/globals"; import { History } from "#core:utils/history"; -import { - type AjaxResponse, - type RemoteResult, - remoteDataSource, - sithSelect2, -} from "#core:utils/select2"; +import type TomSelect from "tom-select"; import { type IdentifiedUserSchema, type PictureSchema, type PicturesFetchIdentificationsResponse, + type PicturesFetchModerationRequestsResponse, type PicturesFetchPicturesData, type UserProfileSchema, picturesDeletePicture, picturesFetchIdentifications, + picturesFetchModerationRequests, picturesFetchPictures, picturesIdentifyUsers, picturesModeratePicture, - userSearchUsers, usersidentifiedDeleteRelation, } from "#openapi"; @@ -27,18 +23,20 @@ import { * able to prefetch its data. */ class PictureWithIdentifications { - identifications: PicturesFetchIdentificationsResponse | null = null; + identifications: PicturesFetchIdentificationsResponse = null; imageLoading = false; identificationsLoading = false; + moderationLoading = false; id: number; // biome-ignore lint/style/useNamingConvention: api is in snake_case compressed_url: string; + moderationRequests: PicturesFetchModerationRequestsResponse = null; constructor(picture: PictureSchema) { Object.assign(this, picture); } - static fromPicture(picture: PictureSchema) { + static fromPicture(picture: PictureSchema): PictureWithIdentifications { return new PictureWithIdentifications(picture); } @@ -46,7 +44,7 @@ class PictureWithIdentifications { * If not already done, fetch the users identified on this picture and * populate the identifications field */ - async loadIdentifications(options?: { forceReload: boolean }) { + async loadIdentifications(options?: { forceReload: boolean }): Promise { if (this.identificationsLoading) { return; // The users are already being fetched. } @@ -65,11 +63,29 @@ class PictureWithIdentifications { this.identificationsLoading = false; } + async loadModeration(options?: { forceReload: boolean }): Promise { + if (this.moderationLoading) { + return; // The moderation requests are already being fetched. + } + if (!!this.moderationRequests && !options?.forceReload) { + // The moderation requests are already fetched + // and the user does not want to force the reload + return; + } + this.moderationLoading = true; + this.moderationRequests = ( + await picturesFetchModerationRequests({ + // biome-ignore lint/style/useNamingConvention: api is in snake_case + path: { picture_id: this.id }, + }) + ).data; + this.moderationLoading = false; + } + /** * Preload the photo and the identifications - * @return {Promise} */ - async preload() { + async preload(): Promise { const img = new Image(); img.src = this.compressed_url; if (!img.complete) { @@ -87,12 +103,12 @@ interface ViewerConfig { userId: number; /** Url of the current album */ albumUrl: string; - /** Id of the album to displlay */ + /** Id of the album to display */ albumId: number; /** id of the first picture to load on the page */ firstPictureId: number; /** if the user is sas admin */ - userIsSasAdmin: number; + userIsSasAdmin: boolean; } /** @@ -103,9 +119,8 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { Alpine.data("picture_viewer", () => ({ /** * All the pictures that can be displayed on this picture viewer - * @type PictureWithIdentifications[] **/ - pictures: [], + pictures: [] as PictureWithIdentifications[], /** * The currently displayed picture * Default dummy data are pre-loaded to avoid javascript error @@ -131,14 +146,12 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { }, /** * The picture which will be displayed next if the user press the "next" button - * @type ?PictureWithIdentifications **/ - nextPicture: null, + nextPicture: null as PictureWithIdentifications, /** * The picture which will be displayed next if the user press the "previous" button - * @type ?PictureWithIdentifications **/ - previousPicture: null, + previousPicture: null as PictureWithIdentifications, /** * The select2 component used to identify users **/ @@ -148,13 +161,11 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { **/ /** * Error message when a moderation operation fails - * @type string **/ moderationError: "", /** * Method of pushing new url to the browser history * Used by popstate event and always reset to it's default value when used - * @type History **/ pushstate: History.Push, @@ -165,20 +176,21 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { query: { album_id: config.albumId }, } as PicturesFetchPicturesData) ).map(PictureWithIdentifications.fromPicture); - this.selector = sithSelect2({ - element: $(this.$refs.search) as unknown as HTMLElement, - dataSource: remoteDataSource(await makeUrl(userSearchUsers), { - excluded: () => [ - ...(this.currentPicture.identifications || []).map( - (i: IdentifiedUserSchema) => i.user.id, - ), - ], - resultConverter: (obj: AjaxResponse) => { - return { ...obj, text: (obj as UserProfileSchema).display_name }; - }, - }), - pictureGetter: (user: RemoteResult) => user.profile_pict, - }); + this.selector = this.$refs.search; + this.selector.filter = (users: UserProfileSchema[]) => { + const resp: UserProfileSchema[] = []; + const ids = [ + ...(this.currentPicture.identifications || []).map( + (i: IdentifiedUserSchema) => i.user.id, + ), + ]; + for (const user of users) { + if (!ids.includes(user.id)) { + resp.push(user); + } + } + return resp; + }; this.currentPicture = this.pictures.find( (i: PictureSchema) => i.id === config.firstPictureId, ); @@ -213,7 +225,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { * and the previous picture, the next picture and * the list of identified users are updated. */ - async updatePicture() { + async updatePicture(): Promise { const updateArgs = { data: { sasPictureId: this.currentPicture.id }, unused: "", @@ -231,16 +243,23 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { } this.moderationError = ""; - const index = this.pictures.indexOf(this.currentPicture); + const index: number = this.pictures.indexOf(this.currentPicture); this.previousPicture = this.pictures[index - 1] || null; this.nextPicture = this.pictures[index + 1] || null; - await this.currentPicture.loadIdentifications(); this.$refs.mainPicture?.addEventListener("load", () => { // once the current picture is loaded, // start preloading the next and previous pictures this.nextPicture?.preload(); this.previousPicture?.preload(); }); + if (this.currentPicture.asked_for_removal && config.userIsSasAdmin) { + await Promise.all([ + this.currentPicture.loadIdentifications(), + this.currentPicture.loadModeration(), + ]); + } else { + await this.currentPicture.loadIdentifications(); + } }, async moderatePicture() { @@ -253,7 +272,7 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { return; } this.currentPicture.is_moderated = true; - this.currentPicture.askedForRemoval = false; + this.currentPicture.asked_for_removal = false; }, async deletePicture() { @@ -277,33 +296,34 @@ exportToHtml("loadViewer", (config: ViewerConfig) => { /** * Send the identification request and update the list of identified users. */ - async submitIdentification() { + async submitIdentification(): Promise { + const widget: TomSelect = this.selector.widget; await picturesIdentifyUsers({ path: { // biome-ignore lint/style/useNamingConvention: api is in snake_case picture_id: this.currentPicture.id, }, - body: this.selector.val().map((i: string) => Number.parseInt(i)), + body: widget.items.map((i: string) => Number.parseInt(i)), }); // refresh the identified users list await this.currentPicture.loadIdentifications({ forceReload: true }); - this.selector.empty().trigger("change"); + + // Clear selection and cache of retrieved user so they can be filtered again + widget.clear(false); + widget.clearOptions(); }, /** * Check if an identification can be removed by the currently logged user - * @param {PictureIdentification} identification - * @return {boolean} */ - canBeRemoved(identification: IdentifiedUserSchema) { + canBeRemoved(identification: IdentifiedUserSchema): boolean { return config.userIsSasAdmin || identification.user.id === config.userId; }, /** * Untag a user from the current picture - * @param {PictureIdentification} identification */ - async removeIdentification(identification: IdentifiedUserSchema) { + async removeIdentification(identification: IdentifiedUserSchema): Promise { const res = await usersidentifiedDeleteRelation({ // biome-ignore lint/style/useNamingConvention: api is in snake_case path: { relation_id: identification.id }, diff --git a/sas/templates/sas/ask_picture_removal.jinja b/sas/templates/sas/ask_picture_removal.jinja new file mode 100644 index 00000000..26c345a0 --- /dev/null +++ b/sas/templates/sas/ask_picture_removal.jinja @@ -0,0 +1,28 @@ +{% extends "core/base.jinja" %} + +{% block title %} + {% trans %}SAS{% endtrans %} +{% endblock %} + + +{% block content %} +

    {% trans %}Image removal request{% endtrans %}

    +
    + {% csrf_token %} + {{ form.non_field_errors() }} +
    + {{ form.reason.help_text }} +
    + {{ form.reason }} +
    +
    + + {% trans %}Cancel{% endtrans %} + + +
    +{% endblock content %} \ No newline at end of file diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index e1d675ba..43651383 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -1,11 +1,12 @@ {% extends "core/base.jinja" %} {%- block additional_css -%} + - {%- endblock -%} {%- block additional_js -%} + {%- endblock -%} @@ -30,10 +31,10 @@