From a098f704240cef9319566771ad33fce29dc87bfb Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 22 Jul 2020 01:46:31 +0200 Subject: [PATCH 01/77] Better import --- apps/scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scripts b/apps/scripts index ee54fca8..6cfae5fd 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit ee54fca89ee247a4ba4af080dd3036d92340eade +Subproject commit 6cfae5fd69deb8beccb342cb589c6d8c594cf1b8 From e3045522d110678e213f78acd471ee09cb14b419 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 22 Jul 2020 21:05:25 +0200 Subject: [PATCH 02/77] Bind GuestTransaction to entry rather than guest (fix OneToOneField) --- apps/activity/models.py | 6 +++--- tox.ini | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/activity/models.py b/apps/activity/models.py index cab229c4..2c014ca5 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -163,7 +163,7 @@ class Entry(models.Model): amount=self.activity.activity_type.guest_entry_fee, reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, valid=True, - guest=self.guest, + entry=self, ).save() return ret @@ -240,8 +240,8 @@ class Guest(models.Model): class GuestTransaction(Transaction): - guest = models.OneToOneField( - Guest, + entry = models.OneToOneField( + Entry, on_delete=models.PROTECT, ) diff --git a/tox.ini b/tox.ini index 48bc3286..eef83e31 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py36-django22 py37-django22 + py38-django22 linters skipsdist = True From 50024dc03dd72e81e05bf33458a6c1c9db500de5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 23 Jul 2020 07:21:49 +0200 Subject: [PATCH 03/77] :bug: Fix NK15 import --- apps/scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scripts b/apps/scripts index 6cfae5fd..f5967359 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit 6cfae5fd69deb8beccb342cb589c6d8c594cf1b8 +Subproject commit f5967359a95b6be8739ba8a43419de06c9bc9587 From 2eb601bd6679df4dfa4adee462bc76632f6d34da Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 17:25:57 +0200 Subject: [PATCH 04/77] :boom: Improve performances --- apps/member/views.py | 6 +- apps/note/models/transactions.py | 6 + apps/note/views.py | 19 +- apps/permission/templatetags/perms.py | 15 +- apps/permission/views.py | 2 +- apps/scripts | 2 +- apps/treasury/forms.py | 3 +- apps/treasury/views.py | 38 +- locale/de/LC_MESSAGES/django.po | 94 +- locale/fr/LC_MESSAGES/django.po | 100 +- note_kfet/settings/base.py | 1 + static/admin/js/vendor/jquery/LICENSE.txt | 8 +- static/admin/js/vendor/jquery/jquery.js | 1793 ++++++++++++------- static/admin/js/vendor/jquery/jquery.min.js | 4 +- static/debug_toolbar/css/print.css | 3 + static/debug_toolbar/css/toolbar.css | 646 +++++++ static/debug_toolbar/img/ajax-loader.gif | Bin 0 -> 404 bytes static/debug_toolbar/img/back.png | Bin 0 -> 574 bytes static/debug_toolbar/img/back_hover.png | Bin 0 -> 613 bytes static/debug_toolbar/img/close.png | Bin 0 -> 498 bytes static/debug_toolbar/img/close_hover.png | Bin 0 -> 706 bytes static/debug_toolbar/img/djdt_vertical.png | Bin 0 -> 882 bytes static/debug_toolbar/img/indicator.png | Bin 0 -> 436 bytes static/debug_toolbar/js/redirect.js | 1 + static/debug_toolbar/js/toolbar.js | 324 ++++ static/debug_toolbar/js/toolbar.timer.js | 52 + templates/base.html | 2 +- templates/note/conso_form.html | 34 +- templates/treasury/remittance_list.html | 8 +- 29 files changed, 2387 insertions(+), 774 deletions(-) create mode 100644 static/debug_toolbar/css/print.css create mode 100644 static/debug_toolbar/css/toolbar.css create mode 100644 static/debug_toolbar/img/ajax-loader.gif create mode 100644 static/debug_toolbar/img/back.png create mode 100644 static/debug_toolbar/img/back_hover.png create mode 100644 static/debug_toolbar/img/close.png create mode 100644 static/debug_toolbar/img/close_hover.png create mode 100644 static/debug_toolbar/img/djdt_vertical.png create mode 100644 static/debug_toolbar/img/indicator.png create mode 100644 static/debug_toolbar/js/redirect.js create mode 100644 static/debug_toolbar/js/toolbar.js create mode 100644 static/debug_toolbar/js/toolbar.timer.js diff --git a/apps/member/views.py b/apps/member/views.py index c52522e2..37f04304 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -129,7 +129,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): user = context['user_object'] history_list = \ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ - .order_by("-created_at", "-id")\ + .order_by("-created_at")\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) history_table = HistoryTable(history_list, prefix='transaction-') history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) @@ -155,7 +155,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Filter the user list with the given pattern. """ - qs = super().get_queryset().filter(profile__registration_valid=True) + qs = super().get_queryset().distinct().filter(profile__registration_valid=True) if "search" in self.request.GET: pattern = self.request.GET["search"] @@ -332,7 +332,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ - .order_by('-created_at', '-id') + .order_by('-created_at') history_table = HistoryTable(club_transactions, prefix="history-") history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) context['history_list'] = history_table diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index a8fb3c22..8a7162c3 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -62,6 +62,7 @@ class TransactionTemplate(models.Model): category = models.ForeignKey( TemplateCategory, on_delete=models.PROTECT, + related_name='templates', verbose_name=_('type'), max_length=31, ) @@ -71,6 +72,11 @@ class TransactionTemplate(models.Model): verbose_name=_("display"), ) + highlighted = models.BooleanField( + default=False, + verbose_name=_("highlighted"), + ) + description = models.CharField( verbose_name=_('description'), max_length=255, diff --git a/apps/note/views.py b/apps/note/views.py index 81d58441..b62b0ae1 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -14,7 +14,7 @@ from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin from .forms import TransactionTemplateForm -from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial +from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial from .models.transactions import SpecialTransaction from .tables import HistoryTable, ButtonTable @@ -31,7 +31,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl table_class = HistoryTable def get_queryset(self, **kwargs): - return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20] + return super().get_queryset(**kwargs).order_by("-created_at").all()[:20] def get_context_data(self, **kwargs): """ @@ -121,19 +121,22 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): table_class = HistoryTable def get_queryset(self, **kwargs): - return super().get_queryset(**kwargs).order_by("-created_at", "-id")[:20] + return super().get_queryset(**kwargs).order_by("-created_at")[:20] def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - from django.db.models import Count - buttons = TransactionTemplate.objects.filter( + categories = TemplateCategory.objects.order_by('name').all() + for category in categories: + category.templates_filtered = category.templates.filter( + PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view") + ).filter(display=True).order_by('name').all() + context['categories'] = [cat for cat in categories if cat.templates_filtered] + context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter( PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view") - ).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name') - context['transaction_templates'] = buttons - context['most_used'] = buttons.order_by('-clicks', 'name')[:10] + ).order_by('name').all() context['title'] = _("Consumptions") context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index a89c7f49..101be6e7 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -40,7 +40,7 @@ def not_empty_model_change_list(model_name): @stringfilter -def model_list(model_name, t="view"): +def model_list(model_name, t="view", fetch=True): """ Return the queryset of all visible instances of the given model. """ @@ -49,10 +49,20 @@ def model_list(model_name, t="view"): return False spl = model_name.split(".") ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) - qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all() + qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)) + if fetch: + qs = qs.all() return qs +@stringfilter +def model_list_length(model_name, t="view"): + """ + Return the length of queryset of all visible instances of the given model. + """ + return model_list(model_name, t, False).count() + + def has_perm(perm, obj): return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj) @@ -85,4 +95,5 @@ register = template.Library() register.filter('not_empty_model_list', not_empty_model_list) register.filter('not_empty_model_change_list', not_empty_model_change_list) register.filter('model_list', model_list) +register.filter('model_list_length', model_list_length) register.filter('has_perm', has_perm) diff --git a/apps/permission/views.py b/apps/permission/views.py index ab555c2f..cbd26a19 100644 --- a/apps/permission/views.py +++ b/apps/permission/views.py @@ -19,7 +19,7 @@ class ProtectQuerysetMixin: """ def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct() + return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) def get_form(self, form_class=None): form = super().get_form(form_class) diff --git a/apps/scripts b/apps/scripts index f5967359..580948fe 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit f5967359a95b6be8739ba8a43419de06c9bc9587 +Subproject commit 580948fe1da1904ba6418daafb48a0a64824a11b diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index b09a46c7..85beaadc 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -132,8 +132,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm): # Add submit button self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) - self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\ - .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) + self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) def clean_last_name(self): """ diff --git a/apps/treasury/views.py b/apps/treasury/views.py index f42e5e77..4e64ce94 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -222,23 +222,41 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["opened_remittances"] = RemittanceTable( + opened_remittances = RemittanceTable( data=Remittance.objects.filter(closed=False).filter( - PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) - context["closed_remittances"] = RemittanceTable( - data=Remittance.objects.filter(closed=True).filter( - PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all()) + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), + prefix="opened-remittances-", + ) + opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10) + context["opened_remittances"] = opened_remittances - context["special_transactions_no_remittance"] = SpecialTransactionTable( + closed_remittances = RemittanceTable( + data=Remittance.objects.filter(closed=True).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all(), + prefix="closed-remittances-", + ) + closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10) + context["closed_remittances"] = closed_remittances + + no_remittance_tr = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), specialtransactionproxy__remittance=None).filter( - PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), - exclude=('remittance_remove', )) - context["special_transactions_with_remittance"] = SpecialTransactionTable( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), + exclude=('remittance_remove', ), + prefix="no-remittance-", + ) + no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10) + context["special_transactions_no_remittance"] = no_remittance_tr + + with_remittance_tr = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), specialtransactionproxy__remittance__closed=False).filter( PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), - exclude=('remittance_add', )) + exclude=('remittance_add', ), + prefix="with-remittance-", + ) + with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10) + context["special_transactions_with_remittance"] = with_remittance_tr return context diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index d4fe6420..915cfbfb 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-07 20:56+0200\n" +"POT-Creation-Date: 2020-07-25 16:50+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,7 +46,7 @@ msgstr "" #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/member/models.py:255 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:249 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:261 #: apps/wei/models.py:64 templates/member/club_info.html:13 #: templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -70,7 +70,7 @@ msgstr "" msgid "activity types" msgstr "" -#: apps/activity/models.py:53 apps/note/models/transactions.py:75 +#: apps/activity/models.py:53 apps/note/models/transactions.py:81 #: apps/permission/models.py:103 apps/permission/models.py:182 #: apps/wei/models.py:70 apps/wei/models.py:126 #: templates/activity/activity_detail.html:16 @@ -78,7 +78,7 @@ msgid "description" msgstr "" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:65 apps/permission/models.py:157 +#: apps/note/models/transactions.py:66 apps/permission/models.py:157 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "" @@ -106,7 +106,7 @@ msgstr "" msgid "end date" msgstr "" -#: apps/activity/models.py:93 apps/note/models/transactions.py:140 +#: apps/activity/models.py:93 apps/note/models/transactions.py:146 #: templates/activity/activity_detail.html:47 msgid "valid" msgstr "" @@ -201,11 +201,11 @@ msgstr "" msgid "Note" msgstr "" -#: apps/activity/tables.py:83 apps/member/tables.py:41 +#: apps/activity/tables.py:83 apps/member/tables.py:42 msgid "Balance" msgstr "" -#: apps/activity/views.py:46 templates/base.html:120 +#: apps/activity/views.py:44 templates/base.html:120 msgid "Activities" msgstr "" @@ -509,11 +509,11 @@ msgstr "" msgid "fee" msgstr "" -#: apps/member/models.py:320 apps/member/views.py:505 apps/wei/views.py:768 +#: apps/member/models.py:320 apps/member/views.py:521 apps/wei/views.py:768 msgid "User is not a member of the parent club" msgstr "" -#: apps/member/models.py:330 apps/member/views.py:514 +#: apps/member/models.py:330 apps/member/views.py:530 msgid "User is already a member of the club" msgstr "" @@ -530,7 +530,7 @@ msgstr "" msgid "memberships" msgstr "" -#: apps/member/tables.py:112 +#: apps/member/tables.py:113 msgid "Renew" msgstr "" @@ -552,32 +552,32 @@ msgstr "" msgid "Search user" msgstr "" -#: apps/member/views.py:500 apps/wei/views.py:759 +#: apps/member/views.py:516 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" -#: apps/member/views.py:518 +#: apps/member/views.py:534 msgid "The membership must start after {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:523 +#: apps/member/views.py:539 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:540 apps/member/views.py:542 apps/member/views.py:544 +#: apps/member/views.py:556 apps/member/views.py:558 apps/member/views.py:560 #: apps/registration/views.py:289 apps/registration/views.py:291 #: apps/registration/views.py:293 msgid "This field is required." msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:100 +#: apps/note/admin.py:120 apps/note/models/transactions.py:106 msgid "source" msgstr "" #: apps/note/admin.py:128 apps/note/admin.py:170 -#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:113 +#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:119 msgid "destination" msgstr "" @@ -619,7 +619,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:123 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:129 msgid "created at" msgstr "" @@ -702,7 +702,7 @@ msgstr "" msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:131 +#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:137 msgid "amount" msgstr "" @@ -710,78 +710,94 @@ msgstr "" msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:71 +#: apps/note/models/transactions.py:72 msgid "display" msgstr "" -#: apps/note/models/transactions.py:81 +#: apps/note/models/transactions.py:77 +msgid "highlighted" +msgstr "" + +#: apps/note/models/transactions.py:87 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:82 +#: apps/note/models/transactions.py:88 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:106 apps/note/models/transactions.py:119 +#: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "" -#: apps/note/models/transactions.py:127 +#: apps/note/models/transactions.py:133 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:135 +#: apps/note/models/transactions.py:141 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:145 apps/note/tables.py:95 +#: apps/note/models/transactions.py:151 apps/note/tables.py:95 msgid "invalidity reason" msgstr "" -#: apps/note/models/transactions.py:153 +#: apps/note/models/transactions.py:159 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:154 +#: apps/note/models/transactions.py:160 #: templates/treasury/sogecredit_detail.html:22 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:216 +#: apps/note/models/transactions.py:175 +msgid "" +"The transaction can't be saved since the source note or the destination note " +"is not active." +msgstr "" + +#: apps/note/models/transactions.py:228 #: templates/activity/activity_entry.html:13 templates/base.html:98 #: templates/note/transaction_form.html:19 #: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:239 +#: apps/note/models/transactions.py:251 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:254 +#: apps/note/models/transactions.py:266 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:259 +#: apps/note/models/transactions.py:271 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:265 +#: apps/note/models/transactions.py:277 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:24 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:265 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:277 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:281 apps/note/models/transactions.py:286 +#: apps/note/models/transactions.py:288 +msgid "" +"A special transaction is only possible between a Note associated to a " +"payment method and a User or a Club" +msgstr "" + +#: apps/note/models/transactions.py:305 apps/note/models/transactions.py:310 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:282 apps/treasury/models.py:227 +#: apps/note/models/transactions.py:306 apps/treasury/models.py:227 msgid "membership transactions" msgstr "" @@ -814,7 +830,7 @@ msgstr "" msgid "Transfer money" msgstr "" -#: apps/note/views.py:137 templates/base.html:93 +#: apps/note/views.py:140 templates/base.html:93 msgid "Consumptions" msgstr "" @@ -1692,7 +1708,7 @@ msgid "Consume!" msgstr "" #: templates/note/conso_form.html:71 -msgid "Most used buttons" +msgid "Highlighted buttons" msgstr "" #: templates/note/conso_form.html:134 @@ -2030,6 +2046,10 @@ msgstr "" msgid "Closed remittances" msgstr "" +#: templates/treasury/remittance_list.html:62 +msgid "There is no closed remittance yet." +msgstr "" + #: templates/treasury/sogecredit_detail.html:29 msgid "total amount" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 4af9cfd5..86072e23 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-05-07 20:56+0200\n" +"POT-Creation-Date: 2020-07-25 16:50+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,7 +47,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/member/models.py:255 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:249 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:261 #: apps/wei/models.py:64 templates/member/club_info.html:13 #: templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -71,7 +71,7 @@ msgstr "type d'activité" msgid "activity types" msgstr "types d'activité" -#: apps/activity/models.py:53 apps/note/models/transactions.py:75 +#: apps/activity/models.py:53 apps/note/models/transactions.py:81 #: apps/permission/models.py:103 apps/permission/models.py:182 #: apps/wei/models.py:70 apps/wei/models.py:126 #: templates/activity/activity_detail.html:16 @@ -79,7 +79,7 @@ msgid "description" msgstr "description" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:65 apps/permission/models.py:157 +#: apps/note/models/transactions.py:66 apps/permission/models.py:157 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" @@ -107,7 +107,7 @@ msgstr "date de début" msgid "end date" msgstr "date de fin" -#: apps/activity/models.py:93 apps/note/models/transactions.py:140 +#: apps/activity/models.py:93 apps/note/models/transactions.py:146 #: templates/activity/activity_detail.html:47 msgid "valid" msgstr "valide" @@ -202,11 +202,11 @@ msgstr "Prénom" msgid "Note" msgstr "Note" -#: apps/activity/tables.py:83 apps/member/tables.py:41 +#: apps/activity/tables.py:83 apps/member/tables.py:42 msgid "Balance" msgstr "Solde du compte" -#: apps/activity/views.py:46 templates/base.html:120 +#: apps/activity/views.py:44 templates/base.html:120 msgid "Activities" msgstr "Activités" @@ -514,11 +514,11 @@ msgstr "l'adhésion finit le" msgid "fee" msgstr "cotisation" -#: apps/member/models.py:320 apps/member/views.py:505 apps/wei/views.py:768 +#: apps/member/models.py:320 apps/member/views.py:521 apps/wei/views.py:768 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" -#: apps/member/models.py:330 apps/member/views.py:514 +#: apps/member/models.py:330 apps/member/views.py:530 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" @@ -535,7 +535,7 @@ msgstr "adhésion" msgid "memberships" msgstr "adhésions" -#: apps/member/tables.py:112 +#: apps/member/tables.py:113 msgid "Renew" msgstr "Renouveler" @@ -557,7 +557,7 @@ msgstr "Un alias avec un nom similaire existe déjà." msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:500 apps/wei/views.py:759 +#: apps/member/views.py:516 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -565,26 +565,26 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:518 +#: apps/member/views.py:534 msgid "The membership must start after {:%m-%d-%Y}." msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." -#: apps/member/views.py:523 +#: apps/member/views.py:539 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." -#: apps/member/views.py:540 apps/member/views.py:542 apps/member/views.py:544 +#: apps/member/views.py:556 apps/member/views.py:558 apps/member/views.py:560 #: apps/registration/views.py:289 apps/registration/views.py:291 #: apps/registration/views.py:293 msgid "This field is required." msgstr "Ce champ est requis." -#: apps/note/admin.py:120 apps/note/models/transactions.py:100 +#: apps/note/admin.py:120 apps/note/models/transactions.py:106 msgid "source" msgstr "source" #: apps/note/admin.py:128 apps/note/admin.py:170 -#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:113 +#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:119 msgid "destination" msgstr "destination" @@ -627,7 +627,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:123 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:129 msgid "created at" msgstr "créée le" @@ -710,7 +710,7 @@ msgstr "catégories de transaction" msgid "A template with this name already exist" msgstr "Un modèle de transaction avec un nom similaire existe déjà." -#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:131 +#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:137 msgid "amount" msgstr "montant" @@ -718,78 +718,94 @@ msgstr "montant" msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:71 +#: apps/note/models/transactions.py:72 msgid "display" msgstr "afficher" -#: apps/note/models/transactions.py:81 +#: apps/note/models/transactions.py:77 +msgid "highlighted" +msgstr "" + +#: apps/note/models/transactions.py:87 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:82 +#: apps/note/models/transactions.py:88 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:106 apps/note/models/transactions.py:119 +#: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "alias utilisé" -#: apps/note/models/transactions.py:127 +#: apps/note/models/transactions.py:133 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:135 +#: apps/note/models/transactions.py:141 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:145 apps/note/tables.py:95 +#: apps/note/models/transactions.py:151 apps/note/tables.py:95 msgid "invalidity reason" msgstr "Motif d'invalidité" -#: apps/note/models/transactions.py:153 +#: apps/note/models/transactions.py:159 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:154 +#: apps/note/models/transactions.py:160 #: templates/treasury/sogecredit_detail.html:22 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:216 +#: apps/note/models/transactions.py:175 +msgid "" +"The transaction can't be saved since the source note or the destination note " +"is not active." +msgstr "" + +#: apps/note/models/transactions.py:228 #: templates/activity/activity_entry.html:13 templates/base.html:98 #: templates/note/transaction_form.html:19 #: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:239 +#: apps/note/models/transactions.py:251 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:254 +#: apps/note/models/transactions.py:266 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:259 +#: apps/note/models/transactions.py:271 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:265 +#: apps/note/models/transactions.py:277 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:265 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:277 templates/note/transaction_form.html:28 msgid "Debit" msgstr "Débit" -#: apps/note/models/transactions.py:281 apps/note/models/transactions.py:286 +#: apps/note/models/transactions.py:288 +msgid "" +"A special transaction is only possible between a Note associated to a " +"payment method and a User or a Club" +msgstr "" + +#: apps/note/models/transactions.py:305 apps/note/models/transactions.py:310 msgid "membership transaction" msgstr "Transaction d'adhésion" -#: apps/note/models/transactions.py:282 apps/treasury/models.py:227 +#: apps/note/models/transactions.py:306 apps/treasury/models.py:227 msgid "membership transactions" msgstr "Transactions d'adhésion" @@ -822,7 +838,7 @@ msgstr "Éditer" msgid "Transfer money" msgstr "Transférer de l'argent" -#: apps/note/views.py:137 templates/base.html:93 +#: apps/note/views.py:140 templates/base.html:93 msgid "Consumptions" msgstr "Consommations" @@ -865,8 +881,8 @@ msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." msgstr "" -"Indique si la permission doit être attribuée même si l'adhésion de l'utilisateur " -"est expirée." +"Indique si la permission doit être attribuée même si l'adhésion de " +"l'utilisateur est expirée." #: apps/permission/models.py:176 templates/permission/all_rights.html:26 msgid "permanent" @@ -1750,8 +1766,8 @@ msgid "Consume!" msgstr "Consommer !" #: templates/note/conso_form.html:71 -msgid "Most used buttons" -msgstr "Boutons les plus utilisés" +msgid "Highlighted buttons" +msgstr "Boutons mis en avant" #: templates/note/conso_form.html:134 msgid "Single consumptions" @@ -2117,6 +2133,10 @@ msgstr "Il n'y a pas de transaction associée à une remise ouverte." msgid "Closed remittances" msgstr "Remises fermées" +#: templates/treasury/remittance_list.html:62 +msgid "There is no closed remittance yet." +msgstr "Il n'y a pas encore de remise fermée." + #: templates/treasury/sogecredit_detail.html:29 msgid "total amount" msgstr "montant total" diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 5ed8b1d8..4733bbad 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -61,6 +61,7 @@ INSTALLED_APPS = [ 'note', 'permission', 'registration', + 'scripts', 'treasury', 'wei', ] diff --git a/static/admin/js/vendor/jquery/LICENSE.txt b/static/admin/js/vendor/jquery/LICENSE.txt index d930e62a..e3dbacb9 100644 --- a/static/admin/js/vendor/jquery/LICENSE.txt +++ b/static/admin/js/vendor/jquery/LICENSE.txt @@ -1,10 +1,4 @@ -Copyright jQuery Foundation and other contributors, https://jquery.org/ - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/jquery/jquery - -==== +Copyright JS Foundation and other contributors, https://js.foundation/ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/static/admin/js/vendor/jquery/jquery.js b/static/admin/js/vendor/jquery/jquery.js index 34a5703d..50937333 100644 --- a/static/admin/js/vendor/jquery/jquery.js +++ b/static/admin/js/vendor/jquery/jquery.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v3.3.1 + * jQuery JavaScript Library v3.5.1 * https://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * https://jquery.org/license * - * Date: 2018-01-20T17:24Z + * Date: 2020-05-04T22:49Z */ ( function( global, factory ) { @@ -47,13 +47,16 @@ var arr = []; -var document = window.document; - var getProto = Object.getPrototypeOf; var slice = arr.slice; -var concat = arr.concat; +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + var push = arr.push; @@ -86,25 +89,40 @@ var isWindow = function isWindow( obj ) { }; +var document = window.document; + var preservedScriptAttributes = { type: true, src: true, + nonce: true, noModule: true }; - function DOMEval( code, doc, node ) { + function DOMEval( code, node, doc ) { doc = doc || document; - var i, + var i, val, script = doc.createElement( "script" ); script.text = code; if ( node ) { for ( i in preservedScriptAttributes ) { - if ( node[ i ] ) { - script[ i ] = node[ i ]; + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); } } } @@ -129,7 +147,7 @@ function toType( obj ) { var - version = "3.3.1", + version = "3.5.1", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -137,11 +155,7 @@ var // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); - }, - - // Support: Android <=4.0 only - // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + }; jQuery.fn = jQuery.prototype = { @@ -207,6 +221,18 @@ jQuery.fn = jQuery.prototype = { return this.eq( -1 ); }, + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); @@ -258,7 +284,6 @@ jQuery.extend = jQuery.fn.extend = function() { // Extend the base object for ( name in options ) { - src = target[ name ]; copy = options[ name ]; // Prevent Object.prototype pollution @@ -270,14 +295,17 @@ jQuery.extend = jQuery.fn.extend = function() { // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; - if ( copyIsArray ) { - copyIsArray = false; - clone = src && Array.isArray( src ) ? src : []; - + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; } else { - clone = src && jQuery.isPlainObject( src ) ? src : {}; + clone = src; } + copyIsArray = false; // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); @@ -330,9 +358,6 @@ jQuery.extend( { }, isEmptyObject: function( obj ) { - - /* eslint-disable no-unused-vars */ - // See https://github.com/eslint/eslint/issues/6125 var name; for ( name in obj ) { @@ -341,9 +366,10 @@ jQuery.extend( { return true; }, - // Evaluates a script in a global context - globalEval: function( code ) { - DOMEval( code ); + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); }, each: function( obj, callback ) { @@ -367,13 +393,6 @@ jQuery.extend( { return obj; }, - // Support: Android <=4.0 only - trim: function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - // results is for internal usage only makeArray: function( arr, results ) { var ret = results || []; @@ -460,7 +479,7 @@ jQuery.extend( { } // Flatten any nested arrays - return concat.apply( [], ret ); + return flat( ret ); }, // A global GUID counter for objects @@ -477,7 +496,7 @@ if ( typeof Symbol === "function" ) { // Populate the class2type map jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), -function( i, name ) { +function( _i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); } ); @@ -499,17 +518,16 @@ function isArrayLike( obj ) { } var Sizzle = /*! - * Sizzle CSS Selector Engine v2.3.3 + * Sizzle CSS Selector Engine v2.3.5 * https://sizzlejs.com/ * - * Copyright jQuery Foundation and other contributors + * Copyright JS Foundation and other contributors * Released under the MIT license - * http://jquery.org/license + * https://js.foundation/ * - * Date: 2016-08-08 + * Date: 2020-03-14 */ -(function( window ) { - +( function( window ) { var i, support, Expr, @@ -540,6 +558,7 @@ var i, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), + nonnativeSelectorCache = createCache(), sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; @@ -548,61 +567,71 @@ var i, }, // Instance methods - hasOwn = ({}).hasOwnProperty, + hasOwn = ( {} ).hasOwnProperty, arr = [], pop = arr.pop, - push_native = arr.push, + pushNative = arr.push, push = arr.push, slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native // https://jsperf.com/thor-indexof-vs-for/5 indexOf = function( list, elem ) { var i = 0, len = list.length; for ( ; i < len; i++ ) { - if ( list[i] === elem ) { + if ( list[ i ] === elem ) { return i; } } return -1; }, - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + + "ismap|loop|multiple|open|readonly|required|scoped", // Regular expressions // http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = "(?:\\\\.|[\\w-]|[^\0-\\xa0])+", + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) "*([*^$|!~]?=)" + whitespace + - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + - "*\\]", + + // "Attribute values must be CSS identifiers [capture 5] + // or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: // 1. quoted (capture 3; capture 4 or capture 5) "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) ".*" + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + + whitespace + "+$", "g" ), rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + + "*" ), + rdescend = new RegExp( whitespace + "|>" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), @@ -613,16 +642,19 @@ var i, "TAG": new RegExp( "^(" + identifier + "|[*])" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + "needsContext": new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, + rhtml = /HTML$/i, rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, @@ -635,18 +667,21 @@ var i, // CSS escapes // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox<24 - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + return nonHex ? + + // Strip the backslash prefix from a non-hex escape sequence + nonHex : + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair high < 0 ? - // BMP codepoint String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); }, @@ -662,7 +697,8 @@ var i, } // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + return ch.slice( 0, -1 ) + "\\" + + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; } // Other potentially-special ASCII characters get backslash-escaped @@ -677,9 +713,9 @@ var i, setDocument(); }, - disabledAncestor = addCombinator( + inDisabledFieldset = addCombinator( function( elem ) { - return elem.disabled === true && ("form" in elem || "label" in elem); + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; }, { dir: "parentNode", next: "legend" } ); @@ -687,18 +723,20 @@ var i, // Optimize for push.apply( _, NodeList ) try { push.apply( - (arr = slice.call( preferredDoc.childNodes )), + ( arr = slice.call( preferredDoc.childNodes ) ), preferredDoc.childNodes ); + // Support: Android<4.0 // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions arr[ preferredDoc.childNodes.length ].nodeType; } catch ( e ) { push = { apply: arr.length ? // Leverage slice if possible function( target, els ) { - push_native.apply( target, slice.call(els) ); + pushNative.apply( target, slice.call( els ) ); } : // Support: IE<9 @@ -706,8 +744,9 @@ try { function( target, els ) { var j = target.length, i = 0; + // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} + while ( ( target[ j++ ] = els[ i++ ] ) ) {} target.length = j - 1; } }; @@ -731,24 +770,21 @@ function Sizzle( selector, context, results, seed ) { // Try to shortcut find operations (as opposed to filters) in HTML documents if ( !seed ) { - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } + setDocument( context ); context = context || document; if ( documentIsHTML ) { // If the selector is sufficiently simple, try using a "get*By*" DOM method // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { // ID selector - if ( (m = match[1]) ) { + if ( ( m = match[ 1 ] ) ) { // Document context if ( nodeType === 9 ) { - if ( (elem = context.getElementById( m )) ) { + if ( ( elem = context.getElementById( m ) ) ) { // Support: IE, Opera, Webkit // TODO: identify versions @@ -767,7 +803,7 @@ function Sizzle( selector, context, results, seed ) { // Support: IE, Opera, Webkit // TODO: identify versions // getElementById can match elements by name instead of ID - if ( newContext && (elem = newContext.getElementById( m )) && + if ( newContext && ( elem = newContext.getElementById( m ) ) && contains( context, elem ) && elem.id === m ) { @@ -777,12 +813,12 @@ function Sizzle( selector, context, results, seed ) { } // Type selector - } else if ( match[2] ) { + } else if ( match[ 2 ] ) { push.apply( results, context.getElementsByTagName( selector ) ); return results; // Class selector - } else if ( (m = match[3]) && support.getElementsByClassName && + } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && context.getElementsByClassName ) { push.apply( results, context.getElementsByClassName( m ) ); @@ -792,50 +828,62 @@ function Sizzle( selector, context, results, seed ) { // Take advantage of querySelectorAll if ( support.qsa && - !compilerCache[ selector + " " ] && - (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && - if ( nodeType !== 1 ) { - newContext = context; - newSelector = selector; - - // qSA looks outside Element context, which is not what we want - // Thanks to Andrew Dupont for this workaround technique - // Support: IE <=8 + // Support: IE 8 only // Exclude object elements - } else if ( context.nodeName.toLowerCase() !== "object" ) { + ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { - // Capture the context ID, setting it first if necessary - if ( (nid = context.getAttribute( "id" )) ) { - nid = nid.replace( rcssescape, fcssescape ); - } else { - context.setAttribute( "id", (nid = expando) ); + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + if ( newContext !== context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = nid.replace( rcssescape, fcssescape ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } } // Prefix every selector in the list groups = tokenize( selector ); i = groups.length; while ( i-- ) { - groups[i] = "#" + nid + " " + toSelector( groups[i] ); + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); } newSelector = groups.join( "," ); - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; } - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); } } } @@ -856,12 +904,14 @@ function createCache() { var keys = []; function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries delete cache[ keys.shift() ]; } - return (cache[ key + " " ] = value); + return ( cache[ key + " " ] = value ); } return cache; } @@ -880,17 +930,19 @@ function markFunction( fn ) { * @param {Function} fn Passed the created element and returns a boolean result */ function assert( fn ) { - var el = document.createElement("fieldset"); + var el = document.createElement( "fieldset" ); try { return !!fn( el ); - } catch (e) { + } catch ( e ) { return false; } finally { + // Remove from its parent by default if ( el.parentNode ) { el.parentNode.removeChild( el ); } + // release memory in IE el = null; } @@ -902,11 +954,11 @@ function assert( fn ) { * @param {Function} handler The method that will be applied */ function addHandle( attrs, handler ) { - var arr = attrs.split("|"), + var arr = attrs.split( "|" ), i = arr.length; while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; + Expr.attrHandle[ arr[ i ] ] = handler; } } @@ -928,7 +980,7 @@ function siblingCheck( a, b ) { // Check if b follows a if ( cur ) { - while ( (cur = cur.nextSibling) ) { + while ( ( cur = cur.nextSibling ) ) { if ( cur === b ) { return -1; } @@ -956,7 +1008,7 @@ function createInputPseudo( type ) { function createButtonPseudo( type ) { return function( elem ) { var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; + return ( name === "input" || name === "button" ) && elem.type === type; }; } @@ -999,7 +1051,7 @@ function createDisabledPseudo( disabled ) { // Where there is no isDisabled, check manually /* jshint -W018 */ elem.isDisabled !== !disabled && - disabledAncestor( elem ) === disabled; + inDisabledFieldset( elem ) === disabled; } return elem.disabled === disabled; @@ -1021,21 +1073,21 @@ function createDisabledPseudo( disabled ) { * @param {Function} fn */ function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { + return markFunction( function( argument ) { argument = +argument; - return markFunction(function( seed, matches ) { + return markFunction( function( seed, matches ) { var j, matchIndexes = fn( [], seed.length, argument ), i = matchIndexes.length; // Match elements found at the specified indexes while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); } } - }); - }); + } ); + } ); } /** @@ -1056,10 +1108,13 @@ support = Sizzle.support = {}; * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; + var namespace = elem.namespaceURI, + docElem = ( elem.ownerDocument || elem ).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); }; /** @@ -1072,7 +1127,11 @@ setDocument = Sizzle.setDocument = function( node ) { doc = node ? node.ownerDocument || node : preferredDoc; // Return early if doc is invalid or already selected - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { return document; } @@ -1081,10 +1140,14 @@ setDocument = Sizzle.setDocument = function( node ) { docElem = document.documentElement; documentIsHTML = !isXML( document ); - // Support: IE 9-11, Edge + // Support: IE 9 - 11+, Edge 12 - 18+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) - if ( preferredDoc !== document && - (subWindow = document.defaultView) && subWindow.top !== subWindow ) { + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { // Support: IE 11, Edge if ( subWindow.addEventListener ) { @@ -1096,25 +1159,36 @@ setDocument = Sizzle.setDocument = function( node ) { } } + // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, + // Safari 4 - 5 only, Opera <=11.6 - 12.x only + // IE/Edge & older browsers don't support the :scope pseudo-class. + // Support: Safari 6.0 only + // Safari 6.0 supports :scope but it's an alias of :root there. + support.scope = assert( function( el ) { + docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); + return typeof el.querySelectorAll !== "undefined" && + !el.querySelectorAll( ":scope fieldset div" ).length; + } ); + /* Attributes ---------------------------------------------------------------------- */ // Support: IE<8 // Verify that getAttribute really returns attributes and not properties // (excepting IE8 booleans) - support.attributes = assert(function( el ) { + support.attributes = assert( function( el ) { el.className = "i"; - return !el.getAttribute("className"); - }); + return !el.getAttribute( "className" ); + } ); /* getElement(s)By* ---------------------------------------------------------------------- */ // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( el ) { - el.appendChild( document.createComment("") ); - return !el.getElementsByTagName("*").length; - }); + support.getElementsByTagName = assert( function( el ) { + el.appendChild( document.createComment( "" ) ); + return !el.getElementsByTagName( "*" ).length; + } ); // Support: IE<9 support.getElementsByClassName = rnative.test( document.getElementsByClassName ); @@ -1123,38 +1197,38 @@ setDocument = Sizzle.setDocument = function( node ) { // Check if getElementById returns elements by name // The broken getElementById methods don't pick up programmatically-set names, // so use a roundabout getElementsByName test - support.getById = assert(function( el ) { + support.getById = assert( function( el ) { docElem.appendChild( el ).id = expando; return !document.getElementsByName || !document.getElementsByName( expando ).length; - }); + } ); // ID filter and find if ( support.getById ) { - Expr.filter["ID"] = function( id ) { + Expr.filter[ "ID" ] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { - return elem.getAttribute("id") === attrId; + return elem.getAttribute( "id" ) === attrId; }; }; - Expr.find["ID"] = function( id, context ) { + Expr.find[ "ID" ] = function( id, context ) { if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var elem = context.getElementById( id ); return elem ? [ elem ] : []; } }; } else { - Expr.filter["ID"] = function( id ) { + Expr.filter[ "ID" ] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode("id"); + elem.getAttributeNode( "id" ); return node && node.value === attrId; }; }; // Support: IE 6 - 7 only // getElementById is not reliable as a find shortcut - Expr.find["ID"] = function( id, context ) { + Expr.find[ "ID" ] = function( id, context ) { if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var node, i, elems, elem = context.getElementById( id ); @@ -1162,7 +1236,7 @@ setDocument = Sizzle.setDocument = function( node ) { if ( elem ) { // Verify the id attribute - node = elem.getAttributeNode("id"); + node = elem.getAttributeNode( "id" ); if ( node && node.value === id ) { return [ elem ]; } @@ -1170,8 +1244,8 @@ setDocument = Sizzle.setDocument = function( node ) { // Fall back on getElementsByName elems = context.getElementsByName( id ); i = 0; - while ( (elem = elems[i++]) ) { - node = elem.getAttributeNode("id"); + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); if ( node && node.value === id ) { return [ elem ]; } @@ -1184,7 +1258,7 @@ setDocument = Sizzle.setDocument = function( node ) { } // Tag - Expr.find["TAG"] = support.getElementsByTagName ? + Expr.find[ "TAG" ] = support.getElementsByTagName ? function( tag, context ) { if ( typeof context.getElementsByTagName !== "undefined" ) { return context.getElementsByTagName( tag ); @@ -1199,12 +1273,13 @@ setDocument = Sizzle.setDocument = function( node ) { var elem, tmp = [], i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too results = context.getElementsByTagName( tag ); // Filter out possible comments if ( tag === "*" ) { - while ( (elem = results[i++]) ) { + while ( ( elem = results[ i++ ] ) ) { if ( elem.nodeType === 1 ) { tmp.push( elem ); } @@ -1216,7 +1291,7 @@ setDocument = Sizzle.setDocument = function( node ) { }; // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { return context.getElementsByClassName( className ); } @@ -1237,10 +1312,14 @@ setDocument = Sizzle.setDocument = function( node ) { // See https://bugs.jquery.com/ticket/13378 rbuggyQSA = []; - if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { + // Build QSA regex // Regex strategy adopted from Diego Perini - assert(function( el ) { + assert( function( el ) { + + var input; + // Select is set to empty string on purpose // This is to test IE's treatment of not explicitly // setting a boolean content attribute, @@ -1254,78 +1333,98 @@ setDocument = Sizzle.setDocument = function( node ) { // Nothing should be selected when empty strings follow ^= or $= or *= // The test attribute must be unknown in Opera but "safe" for WinRT // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( el.querySelectorAll("[msallowcapture^='']").length ) { + if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); } // Support: IE8 // Boolean attributes and "value" are not treated correctly - if ( !el.querySelectorAll("[selected]").length ) { + if ( !el.querySelectorAll( "[selected]" ).length ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push("~="); + rbuggyQSA.push( "~=" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); } // Webkit/Opera - :checked should return selected option elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked // IE8 throws error here and will not see later tests - if ( !el.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); } // Support: Safari 8+, iOS 8+ // https://bugs.webkit.org/show_bug.cgi?id=136851 // In-page `selector#id sibling-combinator selector` fails if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push(".#.+[+~]"); + rbuggyQSA.push( ".#.+[+~]" ); } - }); - assert(function( el ) { + // Support: Firefox <=3.6 - 5 only + // Old Firefox doesn't throw on a badly-escaped identifier. + el.querySelectorAll( "\\\f" ); + rbuggyQSA.push( "[\\r\\n\\f]" ); + } ); + + assert( function( el ) { el.innerHTML = "" + ""; // Support: Windows 8 Native Apps // The type and name attributes are restricted during .innerHTML assignment - var input = document.createElement("input"); + var input = document.createElement( "input" ); input.setAttribute( "type", "hidden" ); el.appendChild( input ).setAttribute( "name", "D" ); // Support: IE8 // Enforce case-sensitivity of name attribute - if ( el.querySelectorAll("[name=d]").length ) { + if ( el.querySelectorAll( "[name=d]" ).length ) { rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) // IE8 throws error here and will not see later tests - if ( el.querySelectorAll(":enabled").length !== 2 ) { + if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { rbuggyQSA.push( ":enabled", ":disabled" ); } // Support: IE9-11+ // IE's :disabled selector does not pick up the children of disabled fieldsets docElem.appendChild( el ).disabled = true; - if ( el.querySelectorAll(":disabled").length !== 2 ) { + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { rbuggyQSA.push( ":enabled", ":disabled" ); } + // Support: Opera 10 - 11 only // Opera 10-11 does not throw on post-comma invalid pseudos - el.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); + el.querySelectorAll( "*,:x" ); + rbuggyQSA.push( ",.*:" ); + } ); } - if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { + docElem.msMatchesSelector ) ) ) ) { + + assert( function( el ) { - assert(function( el ) { // Check to see if it's possible to do matchesSelector // on a disconnected node (IE 9) support.disconnectedMatch = matches.call( el, "*" ); @@ -1334,11 +1433,11 @@ setDocument = Sizzle.setDocument = function( node ) { // Gecko does not error, returns false instead matches.call( el, "[s!='']:x" ); rbuggyMatches.push( "!=", pseudos ); - }); + } ); } - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); /* Contains ---------------------------------------------------------------------- */ @@ -1355,11 +1454,11 @@ setDocument = Sizzle.setDocument = function( node ) { adown.contains ? adown.contains( bup ) : a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); + ) ); } : function( a, b ) { if ( b ) { - while ( (b = b.parentNode) ) { + while ( ( b = b.parentNode ) ) { if ( b === a ) { return true; } @@ -1388,7 +1487,11 @@ setDocument = Sizzle.setDocument = function( node ) { } // Calculate position if both inputs belong to the same document - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? a.compareDocumentPosition( b ) : // Otherwise we know they are disconnected @@ -1396,13 +1499,24 @@ setDocument = Sizzle.setDocument = function( node ) { // Disconnected nodes if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { // Choose the first element that is related to our preferred document - if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a == document || a.ownerDocument == preferredDoc && + contains( preferredDoc, a ) ) { return -1; } - if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b == document || b.ownerDocument == preferredDoc && + contains( preferredDoc, b ) ) { return 1; } @@ -1415,6 +1529,7 @@ setDocument = Sizzle.setDocument = function( node ) { return compare & 4 ? -1 : 1; } : function( a, b ) { + // Exit early if the nodes are identical if ( a === b ) { hasDuplicate = true; @@ -1430,8 +1545,14 @@ setDocument = Sizzle.setDocument = function( node ) { // Parentless nodes are either documents or disconnected if ( !aup || !bup ) { - return a === document ? -1 : - b === document ? 1 : + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + return a == document ? -1 : + b == document ? 1 : + /* eslint-enable eqeqeq */ aup ? -1 : bup ? 1 : sortInput ? @@ -1445,26 +1566,32 @@ setDocument = Sizzle.setDocument = function( node ) { // Otherwise we need full lists of their ancestors for comparison cur = a; - while ( (cur = cur.parentNode) ) { + while ( ( cur = cur.parentNode ) ) { ap.unshift( cur ); } cur = b; - while ( (cur = cur.parentNode) ) { + while ( ( cur = cur.parentNode ) ) { bp.unshift( cur ); } // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { + while ( ap[ i ] === bp[ i ] ) { i++; } return i ? + // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : + siblingCheck( ap[ i ], bp[ i ] ) : // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + /* eslint-disable eqeqeq */ + ap[ i ] == preferredDoc ? -1 : + bp[ i ] == preferredDoc ? 1 : + /* eslint-enable eqeqeq */ 0; }; @@ -1476,16 +1603,10 @@ Sizzle.matches = function( expr, elements ) { }; Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); + setDocument( elem ); if ( support.matchesSelector && documentIsHTML && - !compilerCache[ expr + " " ] && + !nonnativeSelectorCache[ expr + " " ] && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { @@ -1494,32 +1615,46 @@ Sizzle.matchesSelector = function( elem, expr ) { // IE 9's matchesSelector returns false on disconnected nodes if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { return ret; } - } catch (e) {} + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } } return Sizzle( expr, document, null, [ elem ] ).length > 0; }; Sizzle.contains = function( context, elem ) { + // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { setDocument( context ); } return contains( context, elem ); }; Sizzle.attr = function( elem, name ) { + // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { setDocument( elem ); } var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? fn( elem, name, !documentIsHTML ) : @@ -1529,13 +1664,13 @@ Sizzle.attr = function( elem, name ) { val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? + ( val = elem.getAttributeNode( name ) ) && val.specified ? val.value : null; }; Sizzle.escape = function( sel ) { - return (sel + "").replace( rcssescape, fcssescape ); + return ( sel + "" ).replace( rcssescape, fcssescape ); }; Sizzle.error = function( msg ) { @@ -1558,7 +1693,7 @@ Sizzle.uniqueSort = function( results ) { results.sort( sortOrder ); if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { + while ( ( elem = results[ i++ ] ) ) { if ( elem === results[ i ] ) { j = duplicates.push( i ); } @@ -1586,17 +1721,21 @@ getText = Sizzle.getText = function( elem ) { nodeType = elem.nodeType; if ( !nodeType ) { + // If no nodeType, this is expected to be an array - while ( (node = elem[i++]) ) { + while ( ( node = elem[ i++ ] ) ) { + // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { + // Traverse its children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { ret += getText( elem ); @@ -1605,6 +1744,7 @@ getText = Sizzle.getText = function( elem ) { } else if ( nodeType === 3 || nodeType === 4 ) { return elem.nodeValue; } + // Do not include comment or processing instruction nodes return ret; @@ -1632,19 +1772,21 @@ Expr = Sizzle.selectors = { preFilter: { "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || + match[ 5 ] || "" ).replace( runescape, funescape ); - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; } return match.slice( 0, 4 ); }, "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] 1 type (only|nth|...) 2 what (child|of-type) @@ -1655,22 +1797,25 @@ Expr = Sizzle.selectors = { 7 sign of y-component 8 y of y-component */ - match[1] = match[1].toLowerCase(); + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - if ( match[1].slice( 0, 3 ) === "nth" ) { // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); + if ( !match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); } // numeric x and y parameters for Expr.filter.CHILD // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); + // other types prohibit arguments + } else if ( match[ 3 ] ) { + Sizzle.error( match[ 0 ] ); } return match; @@ -1678,26 +1823,28 @@ Expr = Sizzle.selectors = { "PSEUDO": function( match ) { var excess, - unquoted = !match[6] && match[2]; + unquoted = !match[ 6 ] && match[ 2 ]; - if ( matchExpr["CHILD"].test( match[0] ) ) { + if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { return null; } // Accept quoted arguments as-is - if ( match[3] ) { - match[2] = match[4] || match[5] || ""; + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && + ( excess = tokenize( unquoted, true ) ) && + // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); } // Return only captures needed by the pseudo filter method (type and argument) @@ -1710,7 +1857,9 @@ Expr = Sizzle.selectors = { "TAG": function( nodeNameSelector ) { var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); return nodeNameSelector === "*" ? - function() { return true; } : + function() { + return true; + } : function( elem ) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; @@ -1720,10 +1869,16 @@ Expr = Sizzle.selectors = { var pattern = classCache[ className + " " ]; return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); - }); + ( pattern = new RegExp( "(^|" + whitespace + + ")" + className + "(" + whitespace + "|$)" ) ) && classCache( + className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); }, "ATTR": function( name, operator, check ) { @@ -1739,6 +1894,8 @@ Expr = Sizzle.selectors = { result += ""; + /* eslint-disable max-len */ + return operator === "=" ? result === check : operator === "!=" ? result !== check : operator === "^=" ? check && result.indexOf( check ) === 0 : @@ -1747,10 +1904,12 @@ Expr = Sizzle.selectors = { operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : false; + /* eslint-enable max-len */ + }; }, - "CHILD": function( type, what, argument, first, last ) { + "CHILD": function( type, what, _argument, first, last ) { var simple = type.slice( 0, 3 ) !== "nth", forward = type.slice( -4 ) !== "last", ofType = what === "of-type"; @@ -1762,7 +1921,7 @@ Expr = Sizzle.selectors = { return !!elem.parentNode; } : - function( elem, context, xml ) { + function( elem, _context, xml ) { var cache, uniqueCache, outerCache, node, nodeIndex, start, dir = simple !== forward ? "nextSibling" : "previousSibling", parent = elem.parentNode, @@ -1776,7 +1935,7 @@ Expr = Sizzle.selectors = { if ( simple ) { while ( dir ) { node = elem; - while ( (node = node[ dir ]) ) { + while ( ( node = node[ dir ] ) ) { if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { @@ -1784,6 +1943,7 @@ Expr = Sizzle.selectors = { return false; } } + // Reverse direction for :only-* (if we haven't yet done so) start = dir = type === "only" && !start && "nextSibling"; } @@ -1799,22 +1959,22 @@ Expr = Sizzle.selectors = { // ...in a gzip-friendly way node = parent; - outerCache = node[ expando ] || (node[ expando ] = {}); + outerCache = node[ expando ] || ( node[ expando ] = {} ); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); + ( outerCache[ node.uniqueID ] = {} ); cache = uniqueCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; diff = nodeIndex && cache[ 2 ]; node = nodeIndex && parent.childNodes[ nodeIndex ]; - while ( (node = ++nodeIndex && node && node[ dir ] || + while ( ( node = ++nodeIndex && node && node[ dir ] || // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { + ( diff = nodeIndex = 0 ) || start.pop() ) ) { // When found, cache indexes on `parent` and break if ( node.nodeType === 1 && ++diff && node === elem ) { @@ -1824,16 +1984,18 @@ Expr = Sizzle.selectors = { } } else { + // Use previously-cached element index if available if ( useCache ) { + // ...in a gzip-friendly way node = elem; - outerCache = node[ expando ] || (node[ expando ] = {}); + outerCache = node[ expando ] || ( node[ expando ] = {} ); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); + ( outerCache[ node.uniqueID ] = {} ); cache = uniqueCache[ type ] || []; nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; @@ -1843,9 +2005,10 @@ Expr = Sizzle.selectors = { // xml :nth-child(...) // or :nth-last-child(...) or :nth(-last)?-of-type(...) if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { if ( ( ofType ? node.nodeName.toLowerCase() === name : @@ -1854,12 +2017,13 @@ Expr = Sizzle.selectors = { // Cache the index of each encountered element if ( useCache ) { - outerCache = node[ expando ] || (node[ expando ] = {}); + outerCache = node[ expando ] || + ( node[ expando ] = {} ); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) uniqueCache = outerCache[ node.uniqueID ] || - (outerCache[ node.uniqueID ] = {}); + ( outerCache[ node.uniqueID ] = {} ); uniqueCache[ type ] = [ dirruns, diff ]; } @@ -1880,6 +2044,7 @@ Expr = Sizzle.selectors = { }, "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive // http://www.w3.org/TR/selectors/#pseudo-classes // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters @@ -1899,15 +2064,15 @@ Expr = Sizzle.selectors = { if ( fn.length > 1 ) { args = [ pseudo, pseudo, "", argument ]; return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { + markFunction( function( seed, matches ) { var idx, matched = fn( seed, argument ), i = matched.length; while ( i-- ) { - idx = indexOf( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); + idx = indexOf( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); } - }) : + } ) : function( elem ) { return fn( elem, 0, args ); }; @@ -1918,8 +2083,10 @@ Expr = Sizzle.selectors = { }, pseudos: { + // Potentially complex pseudos - "not": markFunction(function( selector ) { + "not": markFunction( function( selector ) { + // Trim the selector passed to compile // to avoid treating leading and trailing // spaces as combinators @@ -1928,39 +2095,40 @@ Expr = Sizzle.selectors = { matcher = compile( selector.replace( rtrim, "$1" ) ); return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { + markFunction( function( seed, matches, _context, xml ) { var elem, unmatched = matcher( seed, null, xml, [] ), i = seed.length; // Match elements unmatched by `matcher` while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); } } - }) : - function( elem, context, xml ) { - input[0] = elem; + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; matcher( input, null, xml, results ); + // Don't keep the element (issue #299) - input[0] = null; + input[ 0 ] = null; return !results.pop(); }; - }), + } ), - "has": markFunction(function( selector ) { + "has": markFunction( function( selector ) { return function( elem ) { return Sizzle( selector, elem ).length > 0; }; - }), + } ), - "contains": markFunction(function( text ) { + "contains": markFunction( function( text ) { text = text.replace( runescape, funescape ); return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; }; - }), + } ), // "Whether an element is represented by a :lang() selector // is based solely on the element's language value @@ -1970,25 +2138,26 @@ Expr = Sizzle.selectors = { // The identifier C does not have to be a valid language name." // http://www.w3.org/TR/selectors/#lang-pseudo "lang": markFunction( function( lang ) { + // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { + if ( !ridentifier.test( lang || "" ) ) { Sizzle.error( "unsupported lang: " + lang ); } lang = lang.replace( runescape, funescape ).toLowerCase(); return function( elem ) { var elemLang; do { - if ( (elemLang = documentIsHTML ? + if ( ( elemLang = documentIsHTML ? elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { elemLang = elemLang.toLowerCase(); return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); return false; }; - }), + } ), // Miscellaneous "target": function( elem ) { @@ -2001,7 +2170,9 @@ Expr = Sizzle.selectors = { }, "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + return elem === document.activeElement && + ( !document.hasFocus || document.hasFocus() ) && + !!( elem.type || elem.href || ~elem.tabIndex ); }, // Boolean properties @@ -2009,16 +2180,20 @@ Expr = Sizzle.selectors = { "disabled": createDisabledPseudo( true ), "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + return ( nodeName === "input" && !!elem.checked ) || + ( nodeName === "option" && !!elem.selected ); }, "selected": function( elem ) { + // Accessing this property makes selected-by-default // options in Safari work properly if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions elem.parentNode.selectedIndex; } @@ -2027,6 +2202,7 @@ Expr = Sizzle.selectors = { // Contents "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), // but not by others (comment: 8; processing instruction: 7; etc.) @@ -2040,7 +2216,7 @@ Expr = Sizzle.selectors = { }, "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); + return !Expr.pseudos[ "empty" ]( elem ); }, // Element/input types @@ -2064,57 +2240,62 @@ Expr = Sizzle.selectors = { // Support: IE<8 // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); }, // Position-in-collection - "first": createPositionalPseudo(function() { + "first": createPositionalPseudo( function() { return [ 0 ]; - }), + } ), - "last": createPositionalPseudo(function( matchIndexes, length ) { + "last": createPositionalPseudo( function( _matchIndexes, length ) { return [ length - 1 ]; - }), + } ), - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { return [ argument < 0 ? argument + length : argument ]; - }), + } ), - "even": createPositionalPseudo(function( matchIndexes, length ) { + "even": createPositionalPseudo( function( matchIndexes, length ) { var i = 0; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; - }), + } ), - "odd": createPositionalPseudo(function( matchIndexes, length ) { + "odd": createPositionalPseudo( function( matchIndexes, length ) { var i = 1; for ( ; i < length; i += 2 ) { matchIndexes.push( i ); } return matchIndexes; - }), + } ), - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; + "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; for ( ; --i >= 0; ) { matchIndexes.push( i ); } return matchIndexes; - }), + } ), - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { var i = argument < 0 ? argument + length : argument; for ( ; ++i < length; ) { matchIndexes.push( i ); } return matchIndexes; - }) + } ) } }; -Expr.pseudos["nth"] = Expr.pseudos["eq"]; +Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; // Add button/input type pseudos for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { @@ -2145,37 +2326,39 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) { while ( soFar ) { // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { if ( match ) { + // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; + soFar = soFar.slice( match[ 0 ].length ) || soFar; } - groups.push( (tokens = []) ); + groups.push( ( tokens = [] ) ); } matched = false; // Combinators - if ( (match = rcombinators.exec( soFar )) ) { + if ( ( match = rcombinators.exec( soFar ) ) ) { matched = match.shift(); - tokens.push({ + tokens.push( { value: matched, + // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); + type: match[ 0 ].replace( rtrim, " " ) + } ); soFar = soFar.slice( matched.length ); } // Filters for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { matched = match.shift(); - tokens.push({ + tokens.push( { value: matched, type: type, matches: match - }); + } ); soFar = soFar.slice( matched.length ); } } @@ -2192,6 +2375,7 @@ tokenize = Sizzle.tokenize = function( selector, parseOnly ) { soFar.length : soFar ? Sizzle.error( selector ) : + // Cache the tokens tokenCache( selector, groups ).slice( 0 ); }; @@ -2201,7 +2385,7 @@ function toSelector( tokens ) { len = tokens.length, selector = ""; for ( ; i < len; i++ ) { - selector += tokens[i].value; + selector += tokens[ i ].value; } return selector; } @@ -2214,9 +2398,10 @@ function addCombinator( matcher, combinator, base ) { doneName = done++; return combinator.first ? + // Check against closest ancestor/preceding element function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { + while ( ( elem = elem[ dir ] ) ) { if ( elem.nodeType === 1 || checkNonElements ) { return matcher( elem, context, xml ); } @@ -2231,7 +2416,7 @@ function addCombinator( matcher, combinator, base ) { // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching if ( xml ) { - while ( (elem = elem[ dir ]) ) { + while ( ( elem = elem[ dir ] ) ) { if ( elem.nodeType === 1 || checkNonElements ) { if ( matcher( elem, context, xml ) ) { return true; @@ -2239,27 +2424,29 @@ function addCombinator( matcher, combinator, base ) { } } } else { - while ( (elem = elem[ dir ]) ) { + while ( ( elem = elem[ dir ] ) ) { if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); // Support: IE <9 only // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + uniqueCache = outerCache[ elem.uniqueID ] || + ( outerCache[ elem.uniqueID ] = {} ); if ( skip && skip === elem.nodeName.toLowerCase() ) { elem = elem[ dir ] || elem; - } else if ( (oldCache = uniqueCache[ key ]) && + } else if ( ( oldCache = uniqueCache[ key ] ) && oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { // Assign to newCache so results back-propagate to previous elements - return (newCache[ 2 ] = oldCache[ 2 ]); + return ( newCache[ 2 ] = oldCache[ 2 ] ); } else { + // Reuse newcache so results back-propagate to previous elements uniqueCache[ key ] = newCache; // A match means we're done; a fail means we have to keep checking - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { return true; } } @@ -2275,20 +2462,20 @@ function elementMatcher( matchers ) { function( elem, context, xml ) { var i = matchers.length; while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { + if ( !matchers[ i ]( elem, context, xml ) ) { return false; } } return true; } : - matchers[0]; + matchers[ 0 ]; } function multipleContexts( selector, contexts, results ) { var i = 0, len = contexts.length; for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); + Sizzle( selector, contexts[ i ], results ); } return results; } @@ -2301,7 +2488,7 @@ function condense( unmatched, map, filter, context, xml ) { mapped = map != null; for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { + if ( ( elem = unmatched[ i ] ) ) { if ( !filter || filter( elem, context, xml ) ) { newUnmatched.push( elem ); if ( mapped ) { @@ -2321,14 +2508,18 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS if ( postFinder && !postFinder[ expando ] ) { postFinder = setMatcher( postFinder, postSelector ); } - return markFunction(function( seed, results, context, xml ) { + return markFunction( function( seed, results, context, xml ) { var temp, i, elem, preMap = [], postMap = [], preexisting = results.length, // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + elems = seed || multipleContexts( + selector || "*", + context.nodeType ? [ context ] : context, + [] + ), // Prefilter to get matcher input, preserving a map for seed-results synchronization matcherIn = preFilter && ( seed || !selector ) ? @@ -2336,6 +2527,7 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS elems, matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, postFinder || ( seed ? preFilter : preexisting || postFilter ) ? @@ -2359,8 +2551,8 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS // Un-match failing elements by moving them back to matcherIn i = temp.length; while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); } } } @@ -2368,25 +2560,27 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS if ( seed ) { if ( postFinder || preFilter ) { if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts temp = []; i = matcherOut.length; while ( i-- ) { - if ( (elem = matcherOut[i]) ) { + if ( ( elem = matcherOut[ i ] ) ) { + // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); + temp.push( ( matcherIn[ i ] = elem ) ); } } - postFinder( null, (matcherOut = []), temp, xml ); + postFinder( null, ( matcherOut = [] ), temp, xml ); } // Move matched elements from seed to results to keep them synchronized i = matcherOut.length; while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { - seed[temp] = !(results[temp] = elem); + seed[ temp ] = !( results[ temp ] = elem ); } } } @@ -2404,14 +2598,14 @@ function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postS push.apply( results, matcherOut ); } } - }); + } ); } function matcherFromTokens( tokens ) { var checkContext, matcher, j, len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], i = leadingRelative ? 1 : 0, // The foundational matcher ensures that elements are reachable from top-level context(s) @@ -2423,38 +2617,43 @@ function matcherFromTokens( tokens ) { }, implicitRelative, true ), matchers = [ function( elem, context, xml ) { var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? + ( checkContext = context ).nodeType ? matchContext( elem, context, xml ) : matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) checkContext = null; return ret; } ]; for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); // Return special upon seeing a positional matcher if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling j = ++i; for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { + if ( Expr.relative[ tokens[ j ].type ] ) { break; } } return setMatcher( i > 1 && elementMatcher( matchers ), i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens + .slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) ).replace( rtrim, "$1" ), matcher, i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), j < len && toSelector( tokens ) ); } @@ -2475,28 +2674,40 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { unmatched = seed && [], setMatched = [], contextBackup = outermostContext, + // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), len = elems.length; if ( outermost ) { - outermostContext = context === document || context || outermost; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; } // Add elements passing elementMatchers directly to results // Support: IE<9, Safari // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { if ( byElement && elem ) { j = 0; - if ( !context && elem.ownerDocument !== document ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { setDocument( elem ); xml = !documentIsHTML; } - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context || document, xml) ) { + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { results.push( elem ); break; } @@ -2508,8 +2719,9 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { // Track unmatched elements for set filters if ( bySet ) { + // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { + if ( ( elem = !matcher && elem ) ) { matchedCount--; } @@ -2533,16 +2745,17 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { // numerically zero. if ( bySet && i !== matchedCount ) { j = 0; - while ( (matcher = setMatchers[j++]) ) { + while ( ( matcher = setMatchers[ j++ ] ) ) { matcher( unmatched, setMatched, context, xml ); } if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting if ( matchedCount > 0 ) { while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); } } } @@ -2583,13 +2796,14 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { cached = compilerCache[ selector + " " ]; if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element if ( !match ) { match = tokenize( selector ); } i = match.length; while ( i-- ) { - cached = matcherFromTokens( match[i] ); + cached = matcherFromTokens( match[ i ] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { @@ -2598,7 +2812,10 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { } // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + cached = compilerCache( + selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) + ); // Save selector and tokenization cached.selector = selector; @@ -2618,7 +2835,7 @@ compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { select = Sizzle.select = function( selector, context, results, seed ) { var i, tokens, token, type, find, compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); results = results || []; @@ -2627,11 +2844,12 @@ select = Sizzle.select = function( selector, context, results, seed ) { if ( match.length === 1 ) { // Reduce context if the leading compound selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[1].type ] ) { + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + context = ( Expr.find[ "ID" ]( token.matches[ 0 ] + .replace( runescape, funescape ), context ) || [] )[ 0 ]; if ( !context ) { return results; @@ -2644,20 +2862,22 @@ select = Sizzle.select = function( selector, context, results, seed ) { } // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; while ( i-- ) { - token = tokens[i]; + token = tokens[ i ]; // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { + if ( Expr.relative[ ( type = token.type ) ] ) { break; } - if ( (find = Expr.find[ type ]) ) { + if ( ( find = Expr.find[ type ] ) ) { + // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || + context + ) ) ) { // If seed is empty or no tokens remain, we can return early tokens.splice( i, 1 ); @@ -2688,7 +2908,7 @@ select = Sizzle.select = function( selector, context, results, seed ) { // One-time assignments // Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; // Support: Chrome 14-35+ // Always assume duplicates if they aren't passed to the comparison function @@ -2699,58 +2919,59 @@ setDocument(); // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) // Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( el ) { +support.sortDetached = assert( function( el ) { + // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement("fieldset") ) & 1; -}); + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); // Support: IE<8 // Prevent attribute/property "interpolation" // https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( el ) { +if ( !assert( function( el ) { el.innerHTML = ""; - return el.firstChild.getAttribute("href") === "#" ; -}) ) { + return el.firstChild.getAttribute( "href" ) === "#"; +} ) ) { addHandle( "type|href|height|width", function( elem, name, isXML ) { if ( !isXML ) { return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); } - }); + } ); } // Support: IE<9 // Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( el ) { +if ( !support.attributes || !assert( function( el ) { el.innerHTML = ""; el.firstChild.setAttribute( "value", "" ); return el.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { +} ) ) { + addHandle( "value", function( elem, _name, isXML ) { if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { return elem.defaultValue; } - }); + } ); } // Support: IE<9 // Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( el ) { - return el.getAttribute("disabled") == null; -}) ) { +if ( !assert( function( el ) { + return el.getAttribute( "disabled" ) == null; +} ) ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? + ( val = elem.getAttributeNode( name ) ) && val.specified ? val.value : - null; + null; } - }); + } ); } return Sizzle; -})( window ); +} )( window ); @@ -3119,7 +3340,7 @@ jQuery.each( { parents: function( elem ) { return dir( elem, "parentNode" ); }, - parentsUntil: function( elem, i, until ) { + parentsUntil: function( elem, _i, until ) { return dir( elem, "parentNode", until ); }, next: function( elem ) { @@ -3134,10 +3355,10 @@ jQuery.each( { prevAll: function( elem ) { return dir( elem, "previousSibling" ); }, - nextUntil: function( elem, i, until ) { + nextUntil: function( elem, _i, until ) { return dir( elem, "nextSibling", until ); }, - prevUntil: function( elem, i, until ) { + prevUntil: function( elem, _i, until ) { return dir( elem, "previousSibling", until ); }, siblings: function( elem ) { @@ -3147,18 +3368,24 @@ jQuery.each( { return siblings( elem.firstChild ); }, contents: function( elem ) { - if ( nodeName( elem, "iframe" ) ) { - return elem.contentDocument; - } + if ( elem.contentDocument != null && - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { - return jQuery.merge( [], elem.childNodes ); + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { @@ -3490,7 +3717,7 @@ jQuery.extend( { var fns = arguments; return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { + jQuery.each( tuples, function( _i, tuple ) { // Map tuples (progress, done, fail) to arguments (done, fail, progress) var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; @@ -3943,7 +4170,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { // ...except when executing function values } else { bulk = fn; - fn = function( elem, key, value ) { + fn = function( elem, _key, value ) { return bulk.call( jQuery( elem ), value ); }; } @@ -3978,7 +4205,7 @@ var rmsPrefix = /^-ms-/, rdashAlpha = /-([a-z])/g; // Used by camelCase as callback to replace() -function fcamelCase( all, letter ) { +function fcamelCase( _all, letter ) { return letter.toUpperCase(); } @@ -4467,6 +4694,26 @@ var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } var isHiddenWithinTree = function( elem, el ) { // isHiddenWithinTree might be called from jQuery#filter function; @@ -4481,32 +4728,11 @@ var isHiddenWithinTree = function( elem, el ) { // Support: Firefox <=43 - 45 // Disconnected elements can have computed display: none, so first confirm that elem is // in the document. - jQuery.contains( elem.ownerDocument, elem ) && + isAttached( elem ) && jQuery.css( elem, "display" ) === "none"; }; -var swap = function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - function adjustCSS( elem, prop, valueParts, tween ) { @@ -4523,7 +4749,8 @@ function adjustCSS( elem, prop, valueParts, tween ) { unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), // Starting value computation is required for potential unit mismatches - initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && rcssNum.exec( jQuery.css( elem, prop ) ); if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { @@ -4670,17 +4897,46 @@ jQuery.fn.extend( { } ); var rcheckableType = ( /^(?:checkbox|radio)$/i ); -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i ); +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); -// We have to close these tags to support XHTML (#13200) -var wrapMap = { +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; // Support: IE <=9 only - option: [ 1, "" ], + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { // XHTML parsers do not magically insert elements in the // same way that tag soup parsers do. So we cannot shorten @@ -4693,12 +4949,14 @@ var wrapMap = { _default: [ 0, "", "" ] }; -// Support: IE <=9 only -wrapMap.optgroup = wrapMap.option; - wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + function getAll( context, tag ) { @@ -4742,7 +5000,7 @@ function setGlobalEval( elems, refElements ) { var rhtml = /<|&#?\w+;/; function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, contains, j, + var elem, tmp, tag, wrap, attached, j, fragment = context.createDocumentFragment(), nodes = [], i = 0, @@ -4806,13 +5064,13 @@ function buildFragment( elems, context, scripts, selection, ignored ) { continue; } - contains = jQuery.contains( elem.ownerDocument, elem ); + attached = isAttached( elem ); // Append to fragment tmp = getAll( fragment.appendChild( elem ), "script" ); // Preserve script evaluation history - if ( contains ) { + if ( attached ) { setGlobalEval( tmp ); } @@ -4831,34 +5089,6 @@ function buildFragment( elems, context, scripts, selection, ignored ) { } -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (#11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (#14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; -} )(); -var documentElement = document.documentElement; - - - var rkeyEvent = /^key/, rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, @@ -4872,8 +5102,19 @@ function returnFalse() { return false; } +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + // Support: IE <=9 only -// See #13393 for more info +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 function safeActiveElement() { try { return document.activeElement; @@ -4956,8 +5197,8 @@ jQuery.event = { special, handlers, type, namespaces, origType, elemData = dataPriv.get( elem ); - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { return; } @@ -4981,7 +5222,7 @@ jQuery.event = { // Init the element's event structure and main handler, if this is the first if ( !( events = elemData.events ) ) { - events = elemData.events = {}; + events = elemData.events = Object.create( null ); } if ( !( eventHandle = elemData.handle ) ) { eventHandle = elemData.handle = function( e ) { @@ -5139,12 +5380,15 @@ jQuery.event = { dispatch: function( nativeEvent ) { - // Make a writable jQuery.Event from the native event object - var event = jQuery.event.fix( nativeEvent ); - var i, j, ret, matched, handleObj, handlerQueue, args = new Array( arguments.length ), - handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event @@ -5173,9 +5417,10 @@ jQuery.event = { while ( ( handleObj = matched.handlers[ j++ ] ) && !event.isImmediatePropagationStopped() ) { - // Triggered event must either 1) have no namespace, or 2) have namespace(s) - // a subset or equal to those in the bound event (both can have no namespace). - if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; @@ -5299,39 +5544,51 @@ jQuery.event = { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, - focus: { - - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - this.focus(); - return false; - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( this.type === "checkbox" && this.click && nodeName( this, "input" ) ) { - this.click(); - return false; + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; }, - // For cross-browser consistency, don't fire native .click() on links + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack _default: function( event ) { - return nodeName( event.target, "a" ); + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); } }, @@ -5348,6 +5605,93 @@ jQuery.event = { } }; +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + jQuery.removeEvent = function( elem, type, handle ) { // This "if" is needed for plain objects @@ -5460,6 +5804,7 @@ jQuery.each( { shiftKey: true, view: true, "char": true, + code: true, charCode: true, key: true, keyCode: true, @@ -5506,6 +5851,33 @@ jQuery.each( { } }, jQuery.event.addProp ); +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + // Create mouseenter/leave events using mouseover/out and event-time checks // so that event delegation works in jQuery. // Do the same for pointerenter/pointerleave and pointerover/pointerout @@ -5591,13 +5963,6 @@ jQuery.fn.extend( { var - /* eslint-disable max-len */ - - // See https://github.com/eslint/eslint/issues/3229 - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi, - - /* eslint-enable */ - // Support: IE <=10 - 11, Edge 12 - 13 only // In IE/Edge using regex groups here causes severe slowdowns. // See https://connect.microsoft.com/IE/feedback/details/1736512/ @@ -5634,7 +5999,7 @@ function restoreScript( elem ) { } function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + var i, l, type, pdataOld, udataOld, udataCur, events; if ( dest.nodeType !== 1 ) { return; @@ -5642,13 +6007,11 @@ function cloneCopyEvent( src, dest ) { // 1. Copy private data: events, handlers, etc. if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.access( src ); - pdataCur = dataPriv.set( dest, pdataOld ); + pdataOld = dataPriv.get( src ); events = pdataOld.events; if ( events ) { - delete pdataCur.handle; - pdataCur.events = {}; + dataPriv.remove( dest, "handle events" ); for ( type in events ) { for ( i = 0, l = events[ type ].length; i < l; i++ ) { @@ -5684,7 +6047,7 @@ function fixInput( src, dest ) { function domManip( collection, args, callback, ignored ) { // Flatten any nested arrays - args = concat.apply( [], args ); + args = flat( args ); var fragment, first, scripts, hasScripts, node, doc, i = 0, @@ -5756,11 +6119,13 @@ function domManip( collection, args, callback, ignored ) { if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); } } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), doc, node ); + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); } } } @@ -5782,7 +6147,7 @@ function remove( elem, selector, keepData ) { } if ( node.parentNode ) { - if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + if ( keepData && isAttached( node ) ) { setGlobalEval( getAll( node, "script" ) ); } node.parentNode.removeChild( node ); @@ -5794,13 +6159,13 @@ function remove( elem, selector, keepData ) { jQuery.extend( { htmlPrefilter: function( html ) { - return html.replace( rxhtmlTag, "<$1>" ); + return html; }, clone: function( elem, dataAndEvents, deepDataAndEvents ) { var i, l, srcElements, destElements, clone = elem.cloneNode( true ), - inPage = jQuery.contains( elem.ownerDocument, elem ); + inPage = isAttached( elem ); // Fix IE cloning issues if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && @@ -6056,6 +6421,27 @@ var getStyles = function( elem ) { return view.getComputedStyle( elem ); }; +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); @@ -6096,8 +6482,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); // Support: IE 9 only // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) div.style.position = "absolute"; - scrollboxSizeVal = div.offsetWidth === 36 || "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; documentElement.removeChild( container ); @@ -6111,7 +6499,7 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); } var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableMarginLeftVal, + reliableTrDimensionsVal, reliableMarginLeftVal, container = document.createElement( "div" ), div = document.createElement( "div" ); @@ -6146,6 +6534,35 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); scrollboxSize: function() { computeStyleTests(); return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; } } ); } )(); @@ -6168,7 +6585,7 @@ function curCSS( elem, name, computed ) { if ( computed ) { ret = computed.getPropertyValue( name ) || computed[ name ]; - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + if ( ret === "" && !isAttached( elem ) ) { ret = jQuery.style( elem, name ); } @@ -6224,30 +6641,13 @@ function addGetHookIf( conditionFn, hookFn ) { } -var +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }, - - cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style; - -// Return a css property mapped to a potentially vendor prefixed property +// Return a vendor-prefixed property or undefined function vendorPropName( name ) { - // Shortcut for names that are not vendor prefixed - if ( name in emptyStyle ) { - return name; - } - // Check for vendor prefixed names var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), i = cssPrefixes.length; @@ -6260,17 +6660,34 @@ function vendorPropName( name ) { } } -// Return a property mapped along what jQuery.cssProps suggests or to -// a vendor prefixed property. +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property function finalPropName( name ) { - var ret = jQuery.cssProps[ name ]; - if ( !ret ) { - ret = jQuery.cssProps[ name ] = vendorPropName( name ) || name; + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; } - return ret; + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; } -function setPositiveNumber( elem, value, subtract ) { + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { // Any relative (+/-) values have already been // normalized at this point @@ -6341,7 +6758,10 @@ function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computed delta - extra - 0.5 - ) ); + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; } return delta; @@ -6351,9 +6771,16 @@ function getWidthOrHeight( elem, dimension, extra ) { // Start with computed style var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + val = curCSS( elem, dimension, styles ), - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox; + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); // Support: Firefox <=54 // Return a confounding non-pixel value or feign ignorance, as appropriate. @@ -6364,22 +6791,38 @@ function getWidthOrHeight( elem, dimension, extra ) { val = "auto"; } - // Check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = valueIsBorderBox && - ( support.boxSizingReliable() || val === elem.style[ dimension ] ); - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - if ( val === "auto" || - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) { + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || - val = elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ]; + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - // offsetWidth/offsetHeight provide border-box values - valueIsBorderBox = true; + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } } // Normalize "" and auto @@ -6425,6 +6868,13 @@ jQuery.extend( { "flexGrow": true, "flexShrink": true, "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, "lineHeight": true, "opacity": true, "order": true, @@ -6480,7 +6930,9 @@ jQuery.extend( { } // If a number was passed in, add the unit (except for certain CSS properties) - if ( type === "number" ) { + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); } @@ -6554,7 +7006,7 @@ jQuery.extend( { } } ); -jQuery.each( [ "height", "width" ], function( i, dimension ) { +jQuery.each( [ "height", "width" ], function( _i, dimension ) { jQuery.cssHooks[ dimension ] = { get: function( elem, computed, extra ) { if ( computed ) { @@ -6580,18 +7032,29 @@ jQuery.each( [ "height", "width" ], function( i, dimension ) { set: function( elem, value, extra ) { var matches, styles = getStyles( elem ), - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra && boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ); + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; // Account for unreliable border-box dimensions by comparing offset* to computed and // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && support.scrollboxSize() === styles.position ) { + if ( isBorderBox && scrollboxSizeBuggy ) { subtract -= Math.ceil( elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - parseFloat( styles[ dimension ] ) - @@ -6759,9 +7222,9 @@ Tween.propHooks = { // Use .style if available and use plain properties where available. if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && - ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || - jQuery.cssHooks[ tween.prop ] ) ) { + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { tween.elem[ tween.prop ] = tween.now; @@ -7316,7 +7779,7 @@ jQuery.fn.extend( { clearQueue = type; type = undefined; } - if ( clearQueue && type !== false ) { + if ( clearQueue ) { this.queue( type || "fx", [] ); } @@ -7399,7 +7862,7 @@ jQuery.fn.extend( { } } ); -jQuery.each( [ "toggle", "show", "hide" ], function( i, name ) { +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { var cssFn = jQuery.fn[ name ]; jQuery.fn[ name ] = function( speed, easing, callback ) { return speed == null || typeof speed === "boolean" ? @@ -7620,7 +8083,7 @@ boolHook = { } }; -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { var getter = attrHandle[ name ] || jQuery.find.attr; attrHandle[ name ] = function( elem, name, isXML ) { @@ -8244,7 +8707,9 @@ jQuery.extend( jQuery.event, { special.bindType || type; // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] && + handle = ( + dataPriv.get( cur, "events" ) || Object.create( null ) + )[ event.type ] && dataPriv.get( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); @@ -8355,7 +8820,10 @@ if ( !support.focusin ) { jQuery.event.special[ fix ] = { setup: function() { - var doc = this.ownerDocument || this, + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, attaches = dataPriv.access( doc, fix ); if ( !attaches ) { @@ -8364,7 +8832,7 @@ if ( !support.focusin ) { dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); }, teardown: function() { - var doc = this.ownerDocument || this, + var doc = this.ownerDocument || this.document || this, attaches = dataPriv.access( doc, fix ) - 1; if ( !attaches ) { @@ -8380,7 +8848,7 @@ if ( !support.focusin ) { } var location = window.location; -var nonce = Date.now(); +var nonce = { guid: Date.now() }; var rquery = ( /\?/ ); @@ -8468,6 +8936,10 @@ jQuery.param = function( a, traditional ) { encodeURIComponent( value == null ? "" : value ); }; + if ( a == null ) { + return ""; + } + // If an array was passed in, assume that it is an array of form elements. if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { @@ -8508,7 +8980,7 @@ jQuery.fn.extend( { rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && ( this.checked || !rcheckableType.test( type ) ); } ) - .map( function( i, elem ) { + .map( function( _i, elem ) { var val = jQuery( this ).val(); if ( val == null ) { @@ -8970,12 +9442,14 @@ jQuery.extend( { if ( !responseHeaders ) { responseHeaders = {}; while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); } } - match = responseHeaders[ key.toLowerCase() ]; + match = responseHeaders[ key.toLowerCase() + " " ]; } - return match == null ? null : match; + return match == null ? null : match.join( ", " ); }, // Raw string @@ -9119,7 +9593,8 @@ jQuery.extend( { // Add or update anti-cache param if needed if ( s.cache === false ) { cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce++ ) + uncached; + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; } // Put hash and anti-cache on the URL that will be requested (gh-1732) @@ -9252,6 +9727,11 @@ jQuery.extend( { response = ajaxHandleResponses( s, jqXHR, responses ); } + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + // Convert no matter what (that way responseXXX fields are always set) response = ajaxConvert( s, response, jqXHR, isSuccess ); @@ -9342,7 +9822,7 @@ jQuery.extend( { } } ); -jQuery.each( [ "get", "post" ], function( i, method ) { +jQuery.each( [ "get", "post" ], function( _i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // Shift arguments if data argument was omitted @@ -9363,8 +9843,17 @@ jQuery.each( [ "get", "post" ], function( i, method ) { }; } ); +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); -jQuery._evalUrl = function( url ) { + +jQuery._evalUrl = function( url, options, doc ) { return jQuery.ajax( { url: url, @@ -9374,7 +9863,16 @@ jQuery._evalUrl = function( url ) { cache: true, async: false, global: false, - "throws": true + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } } ); }; @@ -9657,24 +10155,21 @@ jQuery.ajaxPrefilter( "script", function( s ) { // Bind script tag hack transport jQuery.ajaxTransport( "script", function( s ) { - // This transport only deals with cross domain requests - if ( s.crossDomain ) { + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { var script, callback; return { send: function( _, complete ) { - script = jQuery( " {% endblock %} diff --git a/templates/treasury/remittance_list.html b/templates/treasury/remittance_list.html index 916c47d8..be8e806e 100644 --- a/templates/treasury/remittance_list.html +++ b/templates/treasury/remittance_list.html @@ -55,5 +55,11 @@

{% trans "Closed remittances" %}

- {% render_table closed_remittances %} + {% if closed_remittances.data %} + {% render_table closed_remittances %} + {% else %} +
+ {% trans "There is no closed remittance yet." %} +
+ {% endif %} {% endblock %} From 3fea17c5559fd864015a6ed874b69d9799df6262 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 17:36:37 +0200 Subject: [PATCH 05/77] :bug: Fix Ansible script --- .gitignore | 3 +++ ansible/roles/2-nk20/tasks/main.yml | 5 ++++- ansible/roles/2-nk20/templates/env_example | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) create mode 120000 ansible/roles/2-nk20/templates/env_example diff --git a/.gitignore b/.gitignore index f9082403..2372bf46 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,11 @@ coverage # Local data secrets.py +.env +map.json *.log media/ + # Virtualenv env/ venv/ diff --git a/ansible/roles/2-nk20/tasks/main.yml b/ansible/roles/2-nk20/tasks/main.yml index 0c6d0213..d150a1b6 100644 --- a/ansible/roles/2-nk20/tasks/main.yml +++ b/ansible/roles/2-nk20/tasks/main.yml @@ -15,7 +15,10 @@ force: true - name: Use default env vars (should be updated!) - command: cp /var/www/note_kfet/.env_example /var/www/note_kfet/.env + template: + src: "env_example" + dest: "/var/www/note_kfet/.env" + mode: 0644 - name: Update permissions for note_kfet dir file: diff --git a/ansible/roles/2-nk20/templates/env_example b/ansible/roles/2-nk20/templates/env_example new file mode 120000 index 00000000..d48f3549 --- /dev/null +++ b/ansible/roles/2-nk20/templates/env_example @@ -0,0 +1 @@ +../../../../.env_example \ No newline at end of file From b212bf4093d27c733425a12667addd69017cae4f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 17:42:32 +0200 Subject: [PATCH 06/77] Fix linters --- apps/treasury/forms.py | 1 - apps/treasury/views.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index 85beaadc..4c761ef2 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -8,7 +8,6 @@ from crispy_forms.layout import Submit from django import forms from django.utils.translation import gettext_lazy as _ from note_kfet.inputs import DatePickerInput, AmountInput -from permission.backends import PermissionBackend from .models import Invoice, Product, Remittance, SpecialTransactionProxy diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 4e64ce94..1ab87f09 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -241,7 +241,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): no_remittance_tr = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), specialtransactionproxy__remittance=None).filter( - PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), exclude=('remittance_remove', ), prefix="no-remittance-", ) From afb35d7ae038a22432faa1106aab355a5a329b6f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 17:42:44 +0200 Subject: [PATCH 07/77] Fix linters --- apps/scripts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scripts b/apps/scripts index 580948fe..441c8b96 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit 580948fe1da1904ba6418daafb48a0a64824a11b +Subproject commit 441c8b9659df820d4c1c5f310898571b0bbde520 From b2e1777fe0832a1a913ec85bb86f0581e8295362 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 17:44:38 +0200 Subject: [PATCH 08/77] Update Django CAS Server --- requirements/cas.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/cas.txt b/requirements/cas.txt index c7c696fb..8a0baa85 100644 --- a/requirements/cas.txt +++ b/requirements/cas.txt @@ -1 +1 @@ -django-cas-server==1.1.0 +django-cas-server==1.2.0 From f0bca6982568a7c623180158da98d45795765792 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 18:18:53 +0200 Subject: [PATCH 09/77] :bug: Minor fixes --- apps/member/views.py | 2 +- apps/registration/views.py | 2 +- apps/wei/forms/registration.py | 2 +- apps/wei/models.py | 1 + apps/wei/tables.py | 2 +- apps/wei/views.py | 2 +- templates/wei/weilist_sample.tex | 1 + 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/member/views.py b/apps/member/views.py index 37f04304..40e0fb0c 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -303,7 +303,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ Filter the user list with the given pattern. """ - qs = super().get_queryset().filter() + qs = super().get_queryset().distinct() if "search" in self.request.GET: pattern = self.request.GET["search"] diff --git a/apps/registration/views.py b/apps/registration/views.py index 2c91a604..a0e62202 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -164,7 +164,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi :param kwargs: :return: """ - qs = super().get_queryset().filter(profile__registration_valid=False) + qs = super().get_queryset().distinct().filter(profile__registration_valid=False) if "search" in self.request.GET: pattern = self.request.GET["search"] diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py index 96555372..9ce3a350 100644 --- a/apps/wei/forms/registration.py +++ b/apps/wei/forms/registration.py @@ -96,7 +96,7 @@ class WEIMembershipForm(forms.ModelForm): class BusForm(forms.ModelForm): class Meta: model = Bus - fields = '__all__' + exclude = ('information_json',) widgets = { "wei": Autocomplete( WEIClub, diff --git a/apps/wei/models.py b/apps/wei/models.py index 9cee0d61..6153a870 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -113,6 +113,7 @@ class BusTeam(models.Model): name = models.CharField( max_length=255, + verbose_name=_("name"), ) color = models.PositiveIntegerField( # Use a color picker to get the hexa code diff --git a/apps/wei/tables.py b/apps/wei/tables.py index 36d09342..e4d09fec 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -144,7 +144,7 @@ class BusTable(tables.Table): ) def render_teams(self, value): - return ", ".join(team.name for team in value.all()) + return ", ".join(team.name for team in value.order_by('name').all()) def render_count(self, value): return str(value) + " " + (str(_("members")) if value > 0 else str(_("member"))) diff --git a/apps/wei/views.py b/apps/wei/views.py index 597a44d4..76a01fae 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -330,7 +330,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): bus = self.object teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \ - .filter(bus=bus).annotate(count=Count("memberships")) + .filter(bus=bus).annotate(count=Count("memberships")).order_by("name") teams_table = BusTeamTable(data=teams, prefix="team-") context["teams"] = teams_table diff --git a/templates/wei/weilist_sample.tex b/templates/wei/weilist_sample.tex index a2ff0755..19eb0cdc 100644 --- a/templates/wei/weilist_sample.tex +++ b/templates/wei/weilist_sample.tex @@ -5,6 +5,7 @@ \usepackage[french]{babel} \usepackage[margin=1.5cm]{geometry} +\usepackage{lmodern} \usepackage{ltablex} \usepackage{tabularx} From 18bdc8044bfa396f5ce00e46e4a1109f9710e1b0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 18:26:18 +0200 Subject: [PATCH 10/77] :bug: Minor fixes --- apps/wei/tables.py | 4 ++-- apps/wei/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/wei/tables.py b/apps/wei/tables.py index e4d09fec..a6bc25c6 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -147,7 +147,7 @@ class BusTable(tables.Table): return ", ".join(team.name for team in value.order_by('name').all()) def render_count(self, value): - return str(value) + " " + (str(_("members")) if value > 0 else str(_("member"))) + return str(value) + " " + (str(_("members")) if value > 1 else str(_("member"))) class Meta: attrs = { @@ -178,7 +178,7 @@ class BusTeamTable(tables.Table): ) def render_count(self, value): - return str(value) + " " + (str(_("members")) if value > 0 else str(_("member"))) + return str(value) + " " + (str(_("members")) if value > 1 else str(_("member"))) count = tables.Column( verbose_name=_("Members count"), diff --git a/apps/wei/views.py b/apps/wei/views.py index 76a01fae..3accb804 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -177,7 +177,7 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi return super().dispatch(request, *args, **kwargs) def get_queryset(self, **kwargs): - qs = super().get_queryset(**kwargs).filter(club=self.club) + qs = super().get_queryset(**kwargs).filter(club=self.club).distinct() pattern = self.request.GET.get("search", "") @@ -214,7 +214,7 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable return super().dispatch(request, *args, **kwargs) def get_queryset(self, **kwargs): - qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None) + qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct() pattern = self.request.GET.get("search", "") From 0b6cb4ef19fd34d8f26acab8a15dd74b14b423fc Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 19:14:27 +0200 Subject: [PATCH 11/77] Production is not debug --- note_kfet/settings/production.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index f5e133b1..b7b8856e 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -25,7 +25,7 @@ DATABASES = { } # Break it, fix it! -DEBUG = True +DEBUG = False # Mandatory ! ALLOWED_HOSTS = [os.environ.get('NOTE_URL', 'localhost')] @@ -36,7 +36,7 @@ SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS') EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_USE_SSL = False EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.example.org') -EMAIL_PORT = os.getenv('EMAIL_PORT', 443) +EMAIL_PORT = os.getenv('EMAIL_PORT', 465) EMAIL_HOST_USER = os.getenv('EMAIL_USER', 'change_me') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', 'change_me') From fb5e2578af620e74c724f4c7618f9e14a26ec0e9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 19:40:30 +0200 Subject: [PATCH 12/77] Merge Role and RolePermissions --- apps/member/admin.py | 3 +- apps/member/api/serializers.py | 13 +- apps/member/api/urls.py | 3 +- apps/member/api/views.py | 16 +- apps/member/forms.py | 4 +- apps/member/models.py | 20 +- apps/member/views.py | 3 +- apps/permission/admin.py | 10 +- apps/permission/api/serializers.py | 10 +- apps/permission/api/urls.py | 4 +- apps/permission/api/views.py | 10 +- apps/permission/backends.py | 10 +- apps/permission/fixtures/initial.json | 280 +++++++++----------------- apps/permission/models.py | 14 +- apps/permission/views.py | 3 +- apps/registration/views.py | 3 +- apps/wei/models.py | 3 +- 17 files changed, 135 insertions(+), 274 deletions(-) diff --git a/apps/member/admin.py b/apps/member/admin.py index c7c3ead3..2e50e8dc 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -6,7 +6,7 @@ from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from .forms import ProfileForm -from .models import Club, Membership, Profile, Role +from .models import Club, Membership, Profile class ProfileInline(admin.StackedInline): @@ -39,4 +39,3 @@ admin.site.register(User, CustomUserAdmin) # Add other models admin.site.register(Club) admin.site.register(Membership) -admin.site.register(Role) diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py index a956a46b..19b2ff67 100644 --- a/apps/member/api/serializers.py +++ b/apps/member/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import Profile, Club, Role, Membership +from ..models import Profile, Club, Membership class ProfileSerializer(serializers.ModelSerializer): @@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer): fields = '__all__' -class RoleSerializer(serializers.ModelSerializer): - """ - REST API Serializer for Roles. - The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. - """ - - class Meta: - model = Role - fields = '__all__' - - class MembershipSerializer(serializers.ModelSerializer): """ REST API Serializer for Memberships. diff --git a/apps/member/api/urls.py b/apps/member/api/urls.py index 15bb83ca..5fa54472 100644 --- a/apps/member/api/urls.py +++ b/apps/member/api/urls.py @@ -1,7 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet +from .views import ProfileViewSet, ClubViewSet, MembershipViewSet def register_members_urls(router, path): @@ -10,5 +10,4 @@ def register_members_urls(router, path): """ router.register(path + '/profile', ProfileViewSet) router.register(path + '/club', ClubViewSet) - router.register(path + '/role', RoleViewSet) router.register(path + '/membership', MembershipViewSet) diff --git a/apps/member/api/views.py b/apps/member/api/views.py index 57c216a1..3dc07fe1 100644 --- a/apps/member/api/views.py +++ b/apps/member/api/views.py @@ -4,8 +4,8 @@ from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet -from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer -from ..models import Profile, Club, Role, Membership +from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer +from ..models import Profile, Club, Membership class ProfileViewSet(ReadProtectedModelViewSet): @@ -30,18 +30,6 @@ class ClubViewSet(ReadProtectedModelViewSet): search_fields = ['$name', ] -class RoleViewSet(ReadProtectedModelViewSet): - """ - REST API View set. - The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer, - then render it on /api/members/role/ - """ - queryset = Role.objects.all() - serializer_class = RoleSerializer - filter_backends = [SearchFilter] - search_fields = ['$name', ] - - class MembershipViewSet(ReadProtectedModelViewSet): """ REST API View set. diff --git a/apps/member/forms.py b/apps/member/forms.py index e546d652..4bf3b738 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -7,9 +7,9 @@ from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ from note.models import NoteSpecial from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput -from permission.models import PermissionMask +from permission.models import PermissionMask, Role -from .models import Profile, Club, Membership, Role +from .models import Profile, Club, Membership class CustomAuthenticationForm(AuthenticationForm): diff --git a/apps/member/models.py b/apps/member/models.py index 17b8f044..1972002d 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -247,24 +247,6 @@ class Club(models.Model): return reverse_lazy('member:club_detail', args=(self.pk,)) -class Role(models.Model): - """ - Role that an :model:`auth.User` can have in a :model:`member.Club` - """ - name = models.CharField( - verbose_name=_('name'), - max_length=255, - unique=True, - ) - - class Meta: - verbose_name = _('role') - verbose_name_plural = _('roles') - - def __str__(self): - return str(self.name) - - class Membership(models.Model): """ Register the membership of a user to a club, including roles and membership duration. @@ -284,7 +266,7 @@ class Membership(models.Model): ) roles = models.ManyToManyField( - Role, + "permission.Role", verbose_name=_("roles"), ) diff --git a/apps/member/views.py b/apps/member/views.py index 40e0fb0c..ddd5d084 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -22,10 +22,11 @@ from note.models import Alias, NoteUser from note.models.transactions import Transaction, SpecialTransaction from note.tables import HistoryTable, AliasTable from permission.backends import PermissionBackend +from permission.models import Role from permission.views import ProtectQuerysetMixin from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm -from .models import Club, Membership, Role +from .models import Club, Membership from .tables import ClubTable, UserTable, MembershipTable diff --git a/apps/permission/admin.py b/apps/permission/admin.py index 4312f4b0..41e59695 100644 --- a/apps/permission/admin.py +++ b/apps/permission/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin -from .models import Permission, PermissionMask, RolePermissions +from .models import Permission, PermissionMask, Role @admin.register(PermissionMask) @@ -22,9 +22,9 @@ class PermissionAdmin(admin.ModelAdmin): list_display = ('type', 'model', 'field', 'mask', 'description', ) -@admin.register(RolePermissions) -class RolePermissionsAdmin(admin.ModelAdmin): +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): """ - Admin customisation for RolePermissions + Admin customisation for Role """ - list_display = ('role', ) + list_display = ('name', ) diff --git a/apps/permission/api/serializers.py b/apps/permission/api/serializers.py index e30ed7dc..d0823e19 100644 --- a/apps/permission/api/serializers.py +++ b/apps/permission/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import Permission, RolePermissions +from ..models import Permission, Role class PermissionSerializer(serializers.ModelSerializer): @@ -17,12 +17,12 @@ class PermissionSerializer(serializers.ModelSerializer): fields = '__all__' -class RolePermissionsSerializer(serializers.ModelSerializer): +class RoleSerializer(serializers.ModelSerializer): """ - REST API Serializer for RolePermissions types. - The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API. + REST API Serializer for Role types. + The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. """ class Meta: - model = RolePermissions + model = Role fields = '__all__' diff --git a/apps/permission/api/urls.py b/apps/permission/api/urls.py index b5d53466..b1fdb199 100644 --- a/apps/permission/api/urls.py +++ b/apps/permission/api/urls.py @@ -1,7 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import PermissionViewSet, RolePermissionsViewSet +from .views import PermissionViewSet, RoleViewSet def register_permission_urls(router, path): @@ -9,4 +9,4 @@ def register_permission_urls(router, path): Configure router for permission REST API. """ router.register(path + "/permission", PermissionViewSet) - router.register(path + "/roles", RolePermissionsViewSet) + router.register(path + "/roles", RoleViewSet) diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py index 6a068225..1ec67aa3 100644 --- a/apps/permission/api/views.py +++ b/apps/permission/api/views.py @@ -4,8 +4,8 @@ from django_filters.rest_framework import DjangoFilterBackend from api.viewsets import ReadOnlyProtectedModelViewSet -from .serializers import PermissionSerializer, RolePermissionsSerializer -from ..models import Permission, RolePermissions +from .serializers import PermissionSerializer, RoleSerializer +from ..models import Permission, Role class PermissionViewSet(ReadOnlyProtectedModelViewSet): @@ -20,13 +20,13 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet): filterset_fields = ['model', 'type', ] -class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet): +class RoleViewSet(ReadOnlyProtectedModelViewSet): """ REST API View set. The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer then render it on /api/permission/roles/ """ - queryset = RolePermissions.objects.all() - serializer_class = RolePermissionsSerializer + queryset = Role.objects.all() + serializer_class = RoleSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['role', ] diff --git a/apps/permission/backends.py b/apps/permission/backends.py index 2446f211..9dc69d3d 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -37,17 +37,17 @@ class PermissionBackend(ModelBackend): return Permission.objects.none() qs = Permission.objects.annotate( - club=F("rolepermissions__role__membership__club"), - membership=F("rolepermissions__role__membership"), + club=F("role__role__membership__club"), + membership=F("role__role__membership"), ).filter( ( Q( - rolepermissions__role__membership__date_start__lte=timezone.now().today(), - rolepermissions__role__membership__date_end__gte=timezone.now().today(), + role__role__membership__date_start__lte=timezone.now().today(), + role__role__membership__date_end__gte=timezone.now().today(), ) | Q(permanent=True) ) - & Q(rolepermissions__role__membership__user=user) + & Q(role__role__membership__user=user) & Q(type=t) & Q(mask__rank__lte=get_current_session().get("permission_mask", 0)) ) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 25509b56..0b2df73e 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -1,165 +1,4 @@ [ - { - "model": "member.role", - "pk": 1, - "fields": { - "name": "Adh\u00e9rent BDE" - } - }, - { - "model": "member.role", - "pk": 2, - "fields": { - "name": "Adh\u00e9rent Kfet" - } - }, - { - "model": "member.role", - "pk": 3, - "fields": { - "name": "Membre de club" - } - }, - { - "model": "member.role", - "pk": 4, - "fields": { - "name": "Bureau de club" - } - }, - { - "model": "member.role", - "pk": 5, - "fields": { - "name": "Pr\u00e9sident\u00b7e de club" - } - }, - { - "model": "member.role", - "pk": 6, - "fields": { - "name": "Tr\u00e9sorier\u00b7\u00e8re de club" - } - }, - { - "model": "member.role", - "pk": 7, - "fields": { - "name": "Pr\u00e9sident\u00b7e BDE" - } - }, - { - "model": "member.role", - "pk": 8, - "fields": { - "name": "Tr\u00e9sorier\u00b7\u00e8re BDE" - } - }, - { - "model": "member.role", - "pk": 9, - "fields": { - "name": "Respo info" - } - }, - { - "model": "member.role", - "pk": 10, - "fields": { - "name": "GC Kfet" - } - }, - { - "model": "member.role", - "pk": 11, - "fields": { - "name": "Res[pot]" - } - }, - { - "model": "member.role", - "pk": 12, - "fields": { - "name": "GC WEI" - } - }, - { - "model": "member.role", - "pk": 13, - "fields": { - "name": "Chef de bus" - } - }, - { - "model": "member.role", - "pk": 14, - "fields": { - "name": "Chef d'\u00e9quipe" - } - }, - { - "model": "member.role", - "pk": 15, - "fields": { - "name": "\u00c9lectron libre" - } - }, - { - "model": "member.role", - "pk": 16, - "fields": { - "name": "\u00c9lectron libre (avec perm)" - } - }, - { - "model": "member.role", - "pk": 17, - "fields": { - "name": "1A" - } - }, - { - "model": "member.role", - "pk": 18, - "fields": { - "name": "Adhérent WEI" - } - }, - { - "model": "wei.weirole", - "pk": 12, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 13, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 14, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 15, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 16, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 17, - "fields": {} - }, - { - "model": "wei.weirole", - "pk": 18, - "fields": {} - }, { "model": "permission.permissionmask", "pk": 1, @@ -2217,10 +2056,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 1, "fields": { - "role": 1, + "name": "Adh\u00e9rent BDE", "permissions": [ 1, 2, @@ -2241,10 +2080,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 2, "fields": { - "role": 2, + "name": "Adh\u00e9rent Kfet", "permissions": [ 34, 35, @@ -2267,10 +2106,17 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", + "pk": 3, + "fields": { + "name": "Membre de club" + } + }, + { + "model": "permission.role", "pk": 4, "fields": { - "role": 4, + "name": "Bureau de club", "permissions": [ 22, 47, @@ -2279,10 +2125,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 5, "fields": { - "role": 5, + "name": "Pr\u00e9sident\u00b7e de club", "permissions": [ 50, 51, @@ -2291,10 +2137,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 6, "fields": { - "role": 6, + "name": "Tr\u00e9sorier\u00b7\u00e8re de club", "permissions": [ 59, 19, @@ -2309,10 +2155,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 7, "fields": { - "role": 7, + "name": "Pr\u00e9sident\u00b7e BDE", "permissions": [ 24, 25, @@ -2323,10 +2169,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 8, "fields": { - "role": 8, + "name": "Tr\u00e9sorier\u00b7\u00e8re BDE", "permissions": [ 23, 24, @@ -2359,10 +2205,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 9, "fields": { - "role": 9, + "name": "Respo info", "permissions": [ 1, 2, @@ -2494,10 +2340,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 10, "fields": { - "role": 10, + "name": "GC Kfet", "permissions": [ 32, 33, @@ -2521,10 +2367,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 11, "fields": { - "role": 11, + "name": "Res[pot]", "permissions": [ 37, 38, @@ -2538,10 +2384,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 12, "fields": { - "role": 12, + "name": "GC WEI", "permissions": [ 76, 80, @@ -2571,10 +2417,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 13, "fields": { - "role": 13, + "name": "Chef de bus", "permissions": [ 117, 118, @@ -2586,10 +2432,10 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", "pk": 14, "fields": { - "role": 14, + "name": "Chef d'\u00e9quipe", "permissions": [ 116, 123, @@ -2599,10 +2445,31 @@ } }, { - "model": "permission.rolepermissions", + "model": "permission.role", + "pk": 15, + "fields": { + "name": "\u00c9lectron libre" + } + }, + { + "model": "permission.role", "pk": 16, "fields": { - "role": 18, + "name": "\u00c9lectron libre (avec perm)" + } + }, + { + "model": "permission.role", + "pk": 17, + "fields": { + "name": "1A" + } + }, + { + "model": "permission.role", + "pk": 18, + "fields": { + "name": "Adhérent WEI", "permissions": [ 97, 99, @@ -2618,5 +2485,40 @@ 95 ] } + }, + { + "model": "wei.weirole", + "pk": 12, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 13, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 14, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 15, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 16, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 17, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 18, + "fields": {} } ] diff --git a/apps/permission/models.py b/apps/permission/models.py index ed4a90d0..77aacff7 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import F, Q, Model from django.utils.translation import gettext_lazy as _ -from member.models import Role class InstancedPermission: @@ -307,23 +306,22 @@ class Permission(models.Model): return self.description -class RolePermissions(models.Model): +class Role(models.Model): """ Permissions associated with a Role """ - role = models.OneToOneField( - Role, - on_delete=models.PROTECT, - related_name='permissions', - verbose_name=_('role'), + name = models.CharField( + max_length=255, + verbose_name=_("name"), ) + permissions = models.ManyToManyField( Permission, verbose_name=_("permissions"), ) def __str__(self): - return str(self.role) + return self.name class Meta: verbose_name = _("role permissions") diff --git a/apps/permission/views.py b/apps/permission/views.py index cbd26a19..f3ed5641 100644 --- a/apps/permission/views.py +++ b/apps/permission/views.py @@ -5,9 +5,10 @@ from datetime import date from django.forms import HiddenInput from django.utils.translation import gettext_lazy as _ from django.views.generic import UpdateView, TemplateView -from member.models import Role, Membership +from member.models import Membership from .backends import PermissionBackend +from .models import Role class ProtectQuerysetMixin: diff --git a/apps/registration/views.py b/apps/registration/views.py index a0e62202..bca0d217 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -15,10 +15,11 @@ from django.views.generic import CreateView, TemplateView, DetailView from django.views.generic.edit import FormMixin from django_tables2 import SingleTableView from member.forms import ProfileForm -from member.models import Membership, Club, Role +from member.models import Membership, Club from note.models import SpecialTransaction from note.templatetags.pretty_money import pretty_money from permission.backends import PermissionBackend +from permission.models import Role from permission.views import ProtectQuerysetMixin from .forms import SignUpForm, ValidationForm diff --git a/apps/wei/models.py b/apps/wei/models.py index 6153a870..d2afa57f 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -8,8 +8,9 @@ from django.conf import settings from django.contrib.auth.models import User from django.db import models from django.utils.translation import gettext_lazy as _ -from member.models import Role, Club, Membership +from member.models import Club, Membership from note.models import MembershipTransaction +from permission.models import Role class WEIClub(Club): From 55bc288deb11ad29449c2c343eae831b2f239b9f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 19:59:04 +0200 Subject: [PATCH 13/77] Some roles can only be given in some clubs --- apps/member/models.py | 8 + apps/member/views.py | 10 ++ apps/permission/fixtures/initial.json | 30 +++- apps/permission/models.py | 8 + locale/de/LC_MESSAGES/django.po | 223 +++++++++++++------------ locale/fr/LC_MESSAGES/django.po | 229 +++++++++++++------------- 6 files changed, 285 insertions(+), 223 deletions(-) diff --git a/apps/member/models.py b/apps/member/models.py index 1972002d..0384bf79 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -284,6 +284,7 @@ class Membership(models.Model): verbose_name=_('fee'), ) + @property def valid(self): """ A membership is valid if today is between the start and the end date. @@ -301,6 +302,13 @@ class Membership(models.Model): if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists(): raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name) + for role in self.roles.all(): + club = role.for_club + if club is not None: + if club.pk != self.club_id: + raise ValidationError(_('The role {role} does not apply to the club {club}.') + .format(role=role.name, club=club.name)) + created = not self.pk if created: if Membership.objects.filter( diff --git a/apps/member/views.py b/apps/member/views.py index ddd5d084..614c5bc1 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -24,6 +24,7 @@ from note.tables import HistoryTable, AliasTable from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin +from wei.models import WEIClub from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm from .models import Club, Membership @@ -426,6 +427,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ .get(pk=self.kwargs["club_pk"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not isinstance(club, WEIClub)) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() # If the concerned club is the BDE, then we add the option that Société générale pays the membership. @@ -445,6 +448,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): user = old_membership.user form.fields['user'].initial = user form.fields['user'].disabled = True + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not isinstance(club, WEIClub)) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() form.fields['roles'].initial = old_membership.roles.all() form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1) form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \ @@ -635,6 +640,11 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): del form.fields['last_name'] del form.fields['first_name'] del form.fields['bank'] + + club = self.object.club + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=isinstance(club, WEIClub)) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() + return form def get_success_url(self): diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 0b2df73e..a426ab89 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -2059,6 +2059,7 @@ "model": "permission.role", "pk": 1, "fields": { + "for_club": 1, "name": "Adh\u00e9rent BDE", "permissions": [ 1, @@ -2083,6 +2084,7 @@ "model": "permission.role", "pk": 2, "fields": { + "for_club": 2, "name": "Adh\u00e9rent Kfet", "permissions": [ 34, @@ -2109,13 +2111,16 @@ "model": "permission.role", "pk": 3, "fields": { - "name": "Membre de club" + "for_club": null, + "name": "Membre de club", + "permissions": [] } }, { "model": "permission.role", "pk": 4, "fields": { + "for_club": null, "name": "Bureau de club", "permissions": [ 22, @@ -2128,6 +2133,7 @@ "model": "permission.role", "pk": 5, "fields": { + "for_club": null, "name": "Pr\u00e9sident\u00b7e de club", "permissions": [ 50, @@ -2140,6 +2146,7 @@ "model": "permission.role", "pk": 6, "fields": { + "for_club": null, "name": "Tr\u00e9sorier\u00b7\u00e8re de club", "permissions": [ 59, @@ -2158,6 +2165,7 @@ "model": "permission.role", "pk": 7, "fields": { + "for_club": 1, "name": "Pr\u00e9sident\u00b7e BDE", "permissions": [ 24, @@ -2172,6 +2180,7 @@ "model": "permission.role", "pk": 8, "fields": { + "for_club": 1, "name": "Tr\u00e9sorier\u00b7\u00e8re BDE", "permissions": [ 23, @@ -2208,6 +2217,7 @@ "model": "permission.role", "pk": 9, "fields": { + "for_club": 1, "name": "Respo info", "permissions": [ 1, @@ -2343,6 +2353,7 @@ "model": "permission.role", "pk": 10, "fields": { + "for_club": 2, "name": "GC Kfet", "permissions": [ 32, @@ -2370,6 +2381,7 @@ "model": "permission.role", "pk": 11, "fields": { + "for_club": 2, "name": "Res[pot]", "permissions": [ 37, @@ -2387,6 +2399,7 @@ "model": "permission.role", "pk": 12, "fields": { + "for_club": null, "name": "GC WEI", "permissions": [ 76, @@ -2420,6 +2433,7 @@ "model": "permission.role", "pk": 13, "fields": { + "for_club": null, "name": "Chef de bus", "permissions": [ 117, @@ -2435,6 +2449,7 @@ "model": "permission.role", "pk": 14, "fields": { + "for_club": null, "name": "Chef d'\u00e9quipe", "permissions": [ 116, @@ -2448,27 +2463,34 @@ "model": "permission.role", "pk": 15, "fields": { - "name": "\u00c9lectron libre" + "for_club": null, + "name": "\u00c9lectron libre", + "permissions": [] } }, { "model": "permission.role", "pk": 16, "fields": { - "name": "\u00c9lectron libre (avec perm)" + "for_club": null, + "name": "\u00c9lectron libre (avec perm)", + "permissions": [] } }, { "model": "permission.role", "pk": 17, "fields": { - "name": "1A" + "for_club": null, + "name": "1A", + "permissions": [] } }, { "model": "permission.role", "pk": 18, "fields": { + "for_club": null, "name": "Adhérent WEI", "permissions": [ 97, diff --git a/apps/permission/models.py b/apps/permission/models.py index 77aacff7..56ca9685 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -320,6 +320,14 @@ class Role(models.Model): verbose_name=_("permissions"), ) + for_club = models.ForeignKey( + "member.Club", + verbose_name=_("for club"), + on_delete=models.PROTECT, + null=True, + default=None, + ) + def __str__(self): return self.name diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 915cfbfb..8874f620 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-25 16:50+0200\n" +"POT-Creation-Date: 2020-07-25 19:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -44,11 +44,11 @@ msgid "You can't invite more than 3 people to this activity." msgstr "" #: apps/activity/models.py:23 apps/activity/models.py:48 -#: apps/member/models.py:151 apps/member/models.py:255 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:261 -#: apps/wei/models.py:64 templates/member/club_info.html:13 -#: templates/member/profile_info.html:14 +#: apps/member/models.py:151 apps/note/models/notes.py:188 +#: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 +#: apps/note/models/transactions.py:261 apps/permission/models.py:315 +#: apps/wei/models.py:65 apps/wei/models.py:117 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 #: templates/wei/weiclub_info.html:13 templates/wei/weimembership_form.html:18 msgid "name" @@ -71,21 +71,21 @@ msgid "activity types" msgstr "" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:103 apps/permission/models.py:182 -#: apps/wei/models.py:70 apps/wei/models.py:126 +#: apps/permission/models.py:102 apps/permission/models.py:181 +#: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:157 +#: apps/note/models/transactions.py:66 apps/permission/models.py:156 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "" -#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:277 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:259 #: apps/note/models/notes.py:117 apps/treasury/models.py:221 -#: apps/wei/models.py:157 templates/treasury/sogecredit_detail.html:14 +#: apps/wei/models.py:159 templates/treasury/sogecredit_detail.html:14 #: templates/wei/survey.html:16 msgid "user" msgstr "" @@ -187,12 +187,12 @@ msgid "Type" msgstr "" #: apps/activity/tables.py:77 apps/member/forms.py:83 -#: apps/registration/forms.py:64 apps/treasury/forms.py:121 +#: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "" #: apps/activity/tables.py:79 apps/member/forms.py:88 -#: apps/registration/forms.py:69 apps/treasury/forms.py:123 +#: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" msgstr "" @@ -225,7 +225,7 @@ msgstr "" msgid "IP Address" msgstr "" -#: apps/logs/models.py:35 apps/permission/models.py:127 +#: apps/logs/models.py:35 apps/permission/models.py:126 msgid "model" msgstr "" @@ -304,7 +304,7 @@ msgid "Credit amount" msgstr "" #: apps/member/forms.py:93 apps/registration/forms.py:74 -#: apps/treasury/forms.py:125 templates/note/transaction_form.html:132 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "" @@ -480,7 +480,7 @@ msgid "" "members can renew their membership." msgstr "" -#: apps/member/models.py:240 apps/member/models.py:283 +#: apps/member/models.py:240 apps/member/models.py:265 #: apps/note/models/notes.py:139 msgid "club" msgstr "" @@ -489,44 +489,45 @@ msgstr "" msgid "clubs" msgstr "" -#: apps/member/models.py:261 apps/permission/models.py:318 -msgid "role" -msgstr "" - -#: apps/member/models.py:262 apps/member/models.py:288 +#: apps/member/models.py:270 msgid "roles" msgstr "" -#: apps/member/models.py:293 +#: apps/member/models.py:275 msgid "membership starts on" msgstr "" -#: apps/member/models.py:297 +#: apps/member/models.py:279 msgid "membership ends on" msgstr "" -#: apps/member/models.py:302 +#: apps/member/models.py:284 msgid "fee" msgstr "" -#: apps/member/models.py:320 apps/member/views.py:521 apps/wei/views.py:768 +#: apps/member/models.py:303 apps/member/views.py:527 apps/wei/views.py:768 msgid "User is not a member of the parent club" msgstr "" -#: apps/member/models.py:330 apps/member/views.py:530 +#: apps/member/models.py:309 +#, python-brace-format +msgid "The role {role} does not apply to the club {club}." +msgstr "" + +#: apps/member/models.py:320 apps/member/views.py:536 msgid "User is already a member of the club" msgstr "" -#: apps/member/models.py:381 +#: apps/member/models.py:371 #, python-brace-format msgid "Membership of {user} for the club {club}" msgstr "" -#: apps/member/models.py:384 +#: apps/member/models.py:374 msgid "membership" msgstr "" -#: apps/member/models.py:385 +#: apps/member/models.py:375 msgid "memberships" msgstr "" @@ -534,41 +535,41 @@ msgstr "" msgid "Renew" msgstr "" -#: apps/member/views.py:62 apps/registration/forms.py:23 +#: apps/member/views.py:64 apps/registration/forms.py:23 msgid "This address must be valid." msgstr "" -#: apps/member/views.py:65 templates/member/profile_info.html:47 +#: apps/member/views.py:67 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:124 msgid "Update Profile" msgstr "" -#: apps/member/views.py:75 +#: apps/member/views.py:77 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:181 +#: apps/member/views.py:183 msgid "Search user" msgstr "" -#: apps/member/views.py:516 apps/wei/views.py:759 +#: apps/member/views.py:522 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" -#: apps/member/views.py:534 +#: apps/member/views.py:540 msgid "The membership must start after {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:539 +#: apps/member/views.py:545 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:556 apps/member/views.py:558 apps/member/views.py:560 -#: apps/registration/views.py:289 apps/registration/views.py:291 -#: apps/registration/views.py:293 +#: apps/member/views.py:562 apps/member/views.py:564 apps/member/views.py:566 +#: apps/registration/views.py:290 apps/registration/views.py:292 +#: apps/registration/views.py:294 msgid "This field is required." msgstr "" @@ -834,63 +835,67 @@ msgstr "" msgid "Consumptions" msgstr "" -#: apps/permission/models.py:82 +#: apps/permission/models.py:81 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/permission/models.py:84 +#: apps/permission/models.py:83 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:97 +#: apps/permission/models.py:96 msgid "rank" msgstr "" -#: apps/permission/models.py:110 +#: apps/permission/models.py:109 msgid "permission mask" msgstr "" -#: apps/permission/models.py:111 +#: apps/permission/models.py:110 msgid "permission masks" msgstr "" -#: apps/permission/models.py:151 +#: apps/permission/models.py:150 msgid "query" msgstr "" -#: apps/permission/models.py:164 +#: apps/permission/models.py:163 msgid "mask" msgstr "" -#: apps/permission/models.py:170 +#: apps/permission/models.py:169 msgid "field" msgstr "" -#: apps/permission/models.py:175 +#: apps/permission/models.py:174 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." msgstr "" -#: apps/permission/models.py:176 templates/permission/all_rights.html:26 +#: apps/permission/models.py:175 templates/permission/all_rights.html:26 msgid "permanent" msgstr "" -#: apps/permission/models.py:187 +#: apps/permission/models.py:186 msgid "permission" msgstr "" -#: apps/permission/models.py:188 apps/permission/models.py:322 +#: apps/permission/models.py:187 apps/permission/models.py:320 msgid "permissions" msgstr "" -#: apps/permission/models.py:193 +#: apps/permission/models.py:192 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/permission/models.py:329 apps/permission/models.py:330 +#: apps/permission/models.py:325 +msgid "for club" +msgstr "" + +#: apps/permission/models.py:335 apps/permission/models.py:336 msgid "role permissions" msgstr "" @@ -915,7 +920,7 @@ msgid "" "{model_name}." msgstr "" -#: apps/permission/views.py:47 +#: apps/permission/views.py:48 msgid "All rights" msgstr "" @@ -941,31 +946,31 @@ msgstr "" msgid "Join Kfet Club" msgstr "" -#: apps/registration/views.py:78 +#: apps/registration/views.py:79 msgid "Email validation" msgstr "" -#: apps/registration/views.py:124 +#: apps/registration/views.py:125 msgid "Email validation unsuccessful" msgstr "" -#: apps/registration/views.py:135 +#: apps/registration/views.py:136 msgid "Email validation email sent" msgstr "" -#: apps/registration/views.py:188 +#: apps/registration/views.py:189 msgid "Unregistered users" msgstr "" -#: apps/registration/views.py:255 +#: apps/registration/views.py:256 msgid "You must join the BDE." msgstr "" -#: apps/registration/views.py:277 +#: apps/registration/views.py:278 msgid "You must join BDE club before joining Kfet club." msgstr "" -#: apps/registration/views.py:282 +#: apps/registration/views.py:283 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" @@ -974,7 +979,7 @@ msgstr "" msgid "Treasury" msgstr "" -#: apps/treasury/forms.py:85 apps/treasury/forms.py:133 +#: apps/treasury/forms.py:84 apps/treasury/forms.py:132 #: templates/activity/activity_form.html:9 #: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 @@ -986,19 +991,19 @@ msgstr "" msgid "Submit" msgstr "" -#: apps/treasury/forms.py:87 +#: apps/treasury/forms.py:86 msgid "Close" msgstr "" -#: apps/treasury/forms.py:96 +#: apps/treasury/forms.py:95 msgid "Remittance is already closed." msgstr "" -#: apps/treasury/forms.py:101 +#: apps/treasury/forms.py:100 msgid "You can't change the type of the remittance." msgstr "" -#: apps/treasury/forms.py:127 apps/treasury/tables.py:47 +#: apps/treasury/forms.py:126 apps/treasury/tables.py:47 #: apps/treasury/tables.py:113 templates/note/transaction_form.html:95 #: templates/treasury/remittance_form.html:18 msgid "Amount" @@ -1167,13 +1172,13 @@ msgstr "" msgid "No" msgstr "" -#: apps/wei/apps.py:10 apps/wei/models.py:47 apps/wei/models.py:48 -#: apps/wei/models.py:59 apps/wei/models.py:164 templates/base.html:130 +#: apps/wei/apps.py:10 apps/wei/models.py:48 apps/wei/models.py:49 +#: apps/wei/models.py:60 apps/wei/models.py:166 templates/base.html:130 msgid "WEI" msgstr "" -#: apps/wei/forms/registration.py:47 apps/wei/models.py:111 -#: apps/wei/models.py:273 +#: apps/wei/forms/registration.py:47 apps/wei/models.py:112 +#: apps/wei/models.py:275 msgid "bus" msgstr "" @@ -1194,7 +1199,7 @@ msgid "" msgstr "" #: apps/wei/forms/registration.py:61 apps/wei/forms/registration.py:67 -#: apps/wei/models.py:145 +#: apps/wei/models.py:147 msgid "WEI Roles" msgstr "" @@ -1206,151 +1211,151 @@ msgstr "" msgid "This team doesn't belong to the given bus." msgstr "" -#: apps/wei/models.py:22 templates/wei/weiclub_info.html:23 +#: apps/wei/models.py:23 templates/wei/weiclub_info.html:23 msgid "year" msgstr "" -#: apps/wei/models.py:26 templates/wei/weiclub_info.html:17 +#: apps/wei/models.py:27 templates/wei/weiclub_info.html:17 msgid "date start" msgstr "" -#: apps/wei/models.py:30 templates/wei/weiclub_info.html:20 +#: apps/wei/models.py:31 templates/wei/weiclub_info.html:20 msgid "date end" msgstr "" -#: apps/wei/models.py:75 +#: apps/wei/models.py:76 msgid "survey information" msgstr "" -#: apps/wei/models.py:76 +#: apps/wei/models.py:77 msgid "Information about the survey for new members, encoded in JSON" msgstr "" -#: apps/wei/models.py:98 +#: apps/wei/models.py:99 msgid "Bus" msgstr "" -#: apps/wei/models.py:99 templates/wei/weiclub_tables.html:79 +#: apps/wei/models.py:100 templates/wei/weiclub_tables.html:79 msgid "Buses" msgstr "" -#: apps/wei/models.py:119 +#: apps/wei/models.py:121 msgid "color" msgstr "" -#: apps/wei/models.py:120 +#: apps/wei/models.py:122 msgid "The color of the T-Shirt, stored with its number equivalent" msgstr "" -#: apps/wei/models.py:134 +#: apps/wei/models.py:136 msgid "Bus team" msgstr "" -#: apps/wei/models.py:135 +#: apps/wei/models.py:137 msgid "Bus teams" msgstr "" -#: apps/wei/models.py:144 +#: apps/wei/models.py:146 msgid "WEI Role" msgstr "" -#: apps/wei/models.py:169 +#: apps/wei/models.py:171 msgid "Credit from Société générale" msgstr "" -#: apps/wei/models.py:174 +#: apps/wei/models.py:176 msgid "Caution check given" msgstr "" -#: apps/wei/models.py:178 templates/wei/weimembership_form.html:62 +#: apps/wei/models.py:180 templates/wei/weimembership_form.html:62 msgid "birth date" msgstr "" -#: apps/wei/models.py:184 +#: apps/wei/models.py:186 msgid "Male" msgstr "" -#: apps/wei/models.py:185 +#: apps/wei/models.py:187 msgid "Female" msgstr "" -#: apps/wei/models.py:186 +#: apps/wei/models.py:188 msgid "Non binary" msgstr "" -#: apps/wei/models.py:188 templates/wei/weimembership_form.html:59 +#: apps/wei/models.py:190 templates/wei/weimembership_form.html:59 msgid "gender" msgstr "" -#: apps/wei/models.py:194 templates/wei/weimembership_form.html:65 +#: apps/wei/models.py:196 templates/wei/weimembership_form.html:65 msgid "health issues" msgstr "" -#: apps/wei/models.py:199 templates/wei/weimembership_form.html:68 +#: apps/wei/models.py:201 templates/wei/weimembership_form.html:68 msgid "emergency contact name" msgstr "" -#: apps/wei/models.py:204 templates/wei/weimembership_form.html:71 +#: apps/wei/models.py:206 templates/wei/weimembership_form.html:71 msgid "emergency contact phone" msgstr "" -#: apps/wei/models.py:209 templates/wei/weimembership_form.html:74 +#: apps/wei/models.py:211 templates/wei/weimembership_form.html:74 msgid "" "Register on the mailing list to stay informed of the events of the campus (1 " "mail/week)" msgstr "" -#: apps/wei/models.py:214 templates/wei/weimembership_form.html:77 +#: apps/wei/models.py:216 templates/wei/weimembership_form.html:77 msgid "" "Register on the mailing list to stay informed of the sport events of the " "campus (1 mail/week)" msgstr "" -#: apps/wei/models.py:219 templates/wei/weimembership_form.html:80 +#: apps/wei/models.py:221 templates/wei/weimembership_form.html:80 msgid "" "Register on the mailing list to stay informed of the art events of the " "campus (1 mail/week)" msgstr "" -#: apps/wei/models.py:224 templates/wei/weimembership_form.html:56 +#: apps/wei/models.py:226 templates/wei/weimembership_form.html:56 msgid "first year" msgstr "" -#: apps/wei/models.py:225 +#: apps/wei/models.py:227 msgid "Tells if the user is new in the school." msgstr "" -#: apps/wei/models.py:230 +#: apps/wei/models.py:232 msgid "registration information" msgstr "" -#: apps/wei/models.py:231 +#: apps/wei/models.py:233 msgid "" "Information about the registration (buses for old members, survey fot the " "new members), encoded in JSON" msgstr "" -#: apps/wei/models.py:262 +#: apps/wei/models.py:264 msgid "WEI User" msgstr "" -#: apps/wei/models.py:263 +#: apps/wei/models.py:265 msgid "WEI Users" msgstr "" -#: apps/wei/models.py:283 +#: apps/wei/models.py:285 msgid "team" msgstr "" -#: apps/wei/models.py:293 +#: apps/wei/models.py:295 msgid "WEI registration" msgstr "" -#: apps/wei/models.py:297 +#: apps/wei/models.py:299 msgid "WEI membership" msgstr "" -#: apps/wei/models.py:298 +#: apps/wei/models.py:300 msgid "WEI memberships" msgstr "" @@ -1420,15 +1425,15 @@ msgstr "" msgid "Survey WEI" msgstr "" -#: note_kfet/settings/base.py:154 +#: note_kfet/settings/base.py:155 msgid "German" msgstr "" -#: note_kfet/settings/base.py:155 +#: note_kfet/settings/base.py:156 msgid "English" msgstr "" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:157 msgid "French" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 86072e23..9657513b 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-25 16:50+0200\n" +"POT-Creation-Date: 2020-07-25 19:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -45,11 +45,11 @@ msgid "You can't invite more than 3 people to this activity." msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/activity/models.py:23 apps/activity/models.py:48 -#: apps/member/models.py:151 apps/member/models.py:255 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:261 -#: apps/wei/models.py:64 templates/member/club_info.html:13 -#: templates/member/profile_info.html:14 +#: apps/member/models.py:151 apps/note/models/notes.py:188 +#: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 +#: apps/note/models/transactions.py:261 apps/permission/models.py:315 +#: apps/wei/models.py:65 apps/wei/models.py:117 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 #: templates/wei/weiclub_info.html:13 templates/wei/weimembership_form.html:18 msgid "name" @@ -72,21 +72,21 @@ msgid "activity types" msgstr "types d'activité" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:103 apps/permission/models.py:182 -#: apps/wei/models.py:70 apps/wei/models.py:126 +#: apps/permission/models.py:102 apps/permission/models.py:181 +#: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "description" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:157 +#: apps/note/models/transactions.py:66 apps/permission/models.py:156 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" -#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:277 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:259 #: apps/note/models/notes.py:117 apps/treasury/models.py:221 -#: apps/wei/models.py:157 templates/treasury/sogecredit_detail.html:14 +#: apps/wei/models.py:159 templates/treasury/sogecredit_detail.html:14 #: templates/wei/survey.html:16 msgid "user" msgstr "utilisateur" @@ -188,12 +188,12 @@ msgid "Type" msgstr "Type" #: apps/activity/tables.py:77 apps/member/forms.py:83 -#: apps/registration/forms.py:64 apps/treasury/forms.py:121 +#: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "Nom de famille" #: apps/activity/tables.py:79 apps/member/forms.py:88 -#: apps/registration/forms.py:69 apps/treasury/forms.py:123 +#: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" msgstr "Prénom" @@ -226,7 +226,7 @@ msgstr "Logs" msgid "IP Address" msgstr "Adresse IP" -#: apps/logs/models.py:35 apps/permission/models.py:127 +#: apps/logs/models.py:35 apps/permission/models.py:126 msgid "model" msgstr "Modèle" @@ -305,7 +305,7 @@ msgid "Credit amount" msgstr "Montant à créditer" #: apps/member/forms.py:93 apps/registration/forms.py:74 -#: apps/treasury/forms.py:125 templates/note/transaction_form.html:132 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "Banque" @@ -485,7 +485,7 @@ msgstr "" "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "suivante avant que les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:240 apps/member/models.py:283 +#: apps/member/models.py:240 apps/member/models.py:265 #: apps/note/models/notes.py:139 msgid "club" msgstr "club" @@ -494,44 +494,45 @@ msgstr "club" msgid "clubs" msgstr "clubs" -#: apps/member/models.py:261 apps/permission/models.py:318 -msgid "role" -msgstr "rôle" - -#: apps/member/models.py:262 apps/member/models.py:288 +#: apps/member/models.py:270 msgid "roles" msgstr "rôles" -#: apps/member/models.py:293 +#: apps/member/models.py:275 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:297 +#: apps/member/models.py:279 msgid "membership ends on" msgstr "l'adhésion finit le" -#: apps/member/models.py:302 +#: apps/member/models.py:284 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:320 apps/member/views.py:521 apps/wei/views.py:768 +#: apps/member/models.py:303 apps/member/views.py:527 apps/wei/views.py:768 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" -#: apps/member/models.py:330 apps/member/views.py:530 +#: apps/member/models.py:309 +#, python-brace-format +msgid "The role {role} does not apply to the club {club}." +msgstr "Le rôle {role} ne s'applique pas au club {club}." + +#: apps/member/models.py:320 apps/member/views.py:536 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" -#: apps/member/models.py:381 +#: apps/member/models.py:371 #, python-brace-format msgid "Membership of {user} for the club {club}" msgstr "Adhésion de {user} pour le club {club}" -#: apps/member/models.py:384 +#: apps/member/models.py:374 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:385 +#: apps/member/models.py:375 msgid "memberships" msgstr "adhésions" @@ -539,25 +540,25 @@ msgstr "adhésions" msgid "Renew" msgstr "Renouveler" -#: apps/member/views.py:62 apps/registration/forms.py:23 +#: apps/member/views.py:64 apps/registration/forms.py:23 msgid "This address must be valid." msgstr "Cette adresse doit être valide." -#: apps/member/views.py:65 templates/member/profile_info.html:47 +#: apps/member/views.py:67 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:124 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:75 +#: apps/member/views.py:77 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/views.py:181 +#: apps/member/views.py:183 msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:516 apps/wei/views.py:759 +#: apps/member/views.py:522 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -565,17 +566,17 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:534 +#: apps/member/views.py:540 msgid "The membership must start after {:%m-%d-%Y}." msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." -#: apps/member/views.py:539 +#: apps/member/views.py:545 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." -#: apps/member/views.py:556 apps/member/views.py:558 apps/member/views.py:560 -#: apps/registration/views.py:289 apps/registration/views.py:291 -#: apps/registration/views.py:293 +#: apps/member/views.py:562 apps/member/views.py:564 apps/member/views.py:566 +#: apps/registration/views.py:290 apps/registration/views.py:292 +#: apps/registration/views.py:294 msgid "This field is required." msgstr "Ce champ est requis." @@ -724,7 +725,7 @@ msgstr "afficher" #: apps/note/models/transactions.py:77 msgid "highlighted" -msgstr "" +msgstr "mis en avant" #: apps/note/models/transactions.py:87 msgid "transaction template" @@ -765,6 +766,8 @@ msgid "" "The transaction can't be saved since the source note or the destination note " "is not active." msgstr "" +"La transaction ne peut pas être sauvegardée puisque la note source ou la note " +"de destination n'est pas active." #: apps/note/models/transactions.py:228 #: templates/activity/activity_entry.html:13 templates/base.html:98 @@ -800,6 +803,8 @@ msgid "" "A special transaction is only possible between a Note associated to a " "payment method and a User or a Club" msgstr "" +"Une transaction spéciale n'est possible que entre une note associée à " +"un mode de paiement et un utilisateur ou un club." #: apps/note/models/transactions.py:305 apps/note/models/transactions.py:310 msgid "membership transaction" @@ -842,41 +847,41 @@ msgstr "Transférer de l'argent" msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:82 +#: apps/permission/models.py:81 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "Can {type} {model}.{field} in {query}" -#: apps/permission/models.py:84 +#: apps/permission/models.py:83 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "Can {type} {model} in {query}" -#: apps/permission/models.py:97 +#: apps/permission/models.py:96 msgid "rank" msgstr "Rang" -#: apps/permission/models.py:110 +#: apps/permission/models.py:109 msgid "permission mask" msgstr "masque de permissions" -#: apps/permission/models.py:111 +#: apps/permission/models.py:110 msgid "permission masks" msgstr "masques de permissions" -#: apps/permission/models.py:151 +#: apps/permission/models.py:150 msgid "query" msgstr "requête" -#: apps/permission/models.py:164 +#: apps/permission/models.py:163 msgid "mask" msgstr "masque" -#: apps/permission/models.py:170 +#: apps/permission/models.py:169 msgid "field" msgstr "champ" -#: apps/permission/models.py:175 +#: apps/permission/models.py:174 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." @@ -884,25 +889,29 @@ msgstr "" "Indique si la permission doit être attribuée même si l'adhésion de " "l'utilisateur est expirée." -#: apps/permission/models.py:176 templates/permission/all_rights.html:26 +#: apps/permission/models.py:175 templates/permission/all_rights.html:26 msgid "permanent" msgstr "permanent" -#: apps/permission/models.py:187 +#: apps/permission/models.py:186 msgid "permission" msgstr "permission" -#: apps/permission/models.py:188 apps/permission/models.py:322 +#: apps/permission/models.py:187 apps/permission/models.py:320 msgid "permissions" msgstr "permissions" -#: apps/permission/models.py:193 +#: apps/permission/models.py:192 msgid "Specifying field applies only to view and change permission types." msgstr "" "Spécifie le champ concerné, ne fonctionne que pour les permissions view et " "change." -#: apps/permission/models.py:329 apps/permission/models.py:330 +#: apps/permission/models.py:325 +msgid "for club" +msgstr "s'applique au club" + +#: apps/permission/models.py:335 apps/permission/models.py:336 msgid "role permissions" msgstr "Permissions par rôles" @@ -933,7 +942,7 @@ msgstr "" "Vous n'avez pas la permission de supprimer cette instance du modèle " "{app_label}.{model_name}." -#: apps/permission/views.py:47 +#: apps/permission/views.py:48 msgid "All rights" msgstr "Tous les droits" @@ -962,31 +971,31 @@ msgstr "Adhérer au club BDE" msgid "Join Kfet Club" msgstr "Adhérer au club Kfet" -#: apps/registration/views.py:78 +#: apps/registration/views.py:79 msgid "Email validation" msgstr "Validation de l'adresse mail" -#: apps/registration/views.py:124 +#: apps/registration/views.py:125 msgid "Email validation unsuccessful" msgstr " La validation de l'adresse mail a échoué" -#: apps/registration/views.py:135 +#: apps/registration/views.py:136 msgid "Email validation email sent" msgstr "L'email de vérification de l'adresse email a bien été envoyé." -#: apps/registration/views.py:188 +#: apps/registration/views.py:189 msgid "Unregistered users" msgstr "Utilisateurs en attente d'inscription" -#: apps/registration/views.py:255 +#: apps/registration/views.py:256 msgid "You must join the BDE." msgstr "Vous devez adhérer au BDE." -#: apps/registration/views.py:277 +#: apps/registration/views.py:278 msgid "You must join BDE club before joining Kfet club." msgstr "Vous devez adhérer au club BDE avant d'adhérer au club Kfet." -#: apps/registration/views.py:282 +#: apps/registration/views.py:283 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" @@ -997,7 +1006,7 @@ msgstr "" msgid "Treasury" msgstr "Trésorerie" -#: apps/treasury/forms.py:85 apps/treasury/forms.py:133 +#: apps/treasury/forms.py:84 apps/treasury/forms.py:132 #: templates/activity/activity_form.html:9 #: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 @@ -1009,19 +1018,19 @@ msgstr "Trésorerie" msgid "Submit" msgstr "Envoyer" -#: apps/treasury/forms.py:87 +#: apps/treasury/forms.py:86 msgid "Close" msgstr "Fermer" -#: apps/treasury/forms.py:96 +#: apps/treasury/forms.py:95 msgid "Remittance is already closed." msgstr "La remise est déjà fermée." -#: apps/treasury/forms.py:101 +#: apps/treasury/forms.py:100 msgid "You can't change the type of the remittance." msgstr "Vous ne pouvez pas changer le type de la remise." -#: apps/treasury/forms.py:127 apps/treasury/tables.py:47 +#: apps/treasury/forms.py:126 apps/treasury/tables.py:47 #: apps/treasury/tables.py:113 templates/note/transaction_form.html:95 #: templates/treasury/remittance_form.html:18 msgid "Amount" @@ -1192,13 +1201,13 @@ msgstr "Oui" msgid "No" msgstr "Non" -#: apps/wei/apps.py:10 apps/wei/models.py:47 apps/wei/models.py:48 -#: apps/wei/models.py:59 apps/wei/models.py:164 templates/base.html:130 +#: apps/wei/apps.py:10 apps/wei/models.py:48 apps/wei/models.py:49 +#: apps/wei/models.py:60 apps/wei/models.py:166 templates/base.html:130 msgid "WEI" msgstr "WEI" -#: apps/wei/forms/registration.py:47 apps/wei/models.py:111 -#: apps/wei/models.py:273 +#: apps/wei/forms/registration.py:47 apps/wei/models.py:112 +#: apps/wei/models.py:275 msgid "bus" msgstr "Bus" @@ -1224,7 +1233,7 @@ msgstr "" "bus ou électron libre)" #: apps/wei/forms/registration.py:61 apps/wei/forms/registration.py:67 -#: apps/wei/models.py:145 +#: apps/wei/models.py:147 msgid "WEI Roles" msgstr "Rôles au WEI" @@ -1236,97 +1245,97 @@ msgstr "Sélectionnez les rôles qui vous intéressent." msgid "This team doesn't belong to the given bus." msgstr "Cette équipe n'appartient pas à ce bus." -#: apps/wei/models.py:22 templates/wei/weiclub_info.html:23 +#: apps/wei/models.py:23 templates/wei/weiclub_info.html:23 msgid "year" msgstr "année" -#: apps/wei/models.py:26 templates/wei/weiclub_info.html:17 +#: apps/wei/models.py:27 templates/wei/weiclub_info.html:17 msgid "date start" msgstr "début" -#: apps/wei/models.py:30 templates/wei/weiclub_info.html:20 +#: apps/wei/models.py:31 templates/wei/weiclub_info.html:20 msgid "date end" msgstr "fin" -#: apps/wei/models.py:75 +#: apps/wei/models.py:76 msgid "survey information" msgstr "informations sur le questionnaire" -#: apps/wei/models.py:76 +#: apps/wei/models.py:77 msgid "Information about the survey for new members, encoded in JSON" msgstr "" "Informations sur le sondage pour les nouveaux membres, encodées en JSON" -#: apps/wei/models.py:98 +#: apps/wei/models.py:99 msgid "Bus" msgstr "Bus" -#: apps/wei/models.py:99 templates/wei/weiclub_tables.html:79 +#: apps/wei/models.py:100 templates/wei/weiclub_tables.html:79 msgid "Buses" msgstr "Bus" -#: apps/wei/models.py:119 +#: apps/wei/models.py:121 msgid "color" msgstr "couleur" -#: apps/wei/models.py:120 +#: apps/wei/models.py:122 msgid "The color of the T-Shirt, stored with its number equivalent" msgstr "" "La couleur du T-Shirt, stocké sous la forme de son équivalent numérique" -#: apps/wei/models.py:134 +#: apps/wei/models.py:136 msgid "Bus team" msgstr "Équipe de bus" -#: apps/wei/models.py:135 +#: apps/wei/models.py:137 msgid "Bus teams" msgstr "Équipes de bus" -#: apps/wei/models.py:144 +#: apps/wei/models.py:146 msgid "WEI Role" msgstr "Rôle au WEI" -#: apps/wei/models.py:169 +#: apps/wei/models.py:171 msgid "Credit from Société générale" msgstr "Crédit de la Société générale" -#: apps/wei/models.py:174 +#: apps/wei/models.py:176 msgid "Caution check given" msgstr "Chèque de caution donné" -#: apps/wei/models.py:178 templates/wei/weimembership_form.html:62 +#: apps/wei/models.py:180 templates/wei/weimembership_form.html:62 msgid "birth date" msgstr "date de naissance" -#: apps/wei/models.py:184 +#: apps/wei/models.py:186 msgid "Male" msgstr "Homme" -#: apps/wei/models.py:185 +#: apps/wei/models.py:187 msgid "Female" msgstr "Femme" -#: apps/wei/models.py:186 +#: apps/wei/models.py:188 msgid "Non binary" msgstr "Non-binaire" -#: apps/wei/models.py:188 templates/wei/weimembership_form.html:59 +#: apps/wei/models.py:190 templates/wei/weimembership_form.html:59 msgid "gender" msgstr "genre" -#: apps/wei/models.py:194 templates/wei/weimembership_form.html:65 +#: apps/wei/models.py:196 templates/wei/weimembership_form.html:65 msgid "health issues" msgstr "problèmes de santé" -#: apps/wei/models.py:199 templates/wei/weimembership_form.html:68 +#: apps/wei/models.py:201 templates/wei/weimembership_form.html:68 msgid "emergency contact name" msgstr "Nom du contact en cas d'urgence" -#: apps/wei/models.py:204 templates/wei/weimembership_form.html:71 +#: apps/wei/models.py:206 templates/wei/weimembership_form.html:71 msgid "emergency contact phone" msgstr "Téléphone du contact en cas d'urgence" -#: apps/wei/models.py:209 templates/wei/weimembership_form.html:74 +#: apps/wei/models.py:211 templates/wei/weimembership_form.html:74 msgid "" "Register on the mailing list to stay informed of the events of the campus (1 " "mail/week)" @@ -1334,7 +1343,7 @@ msgstr "" "S'inscrire sur la liste de diffusion pour rester informé des événements sur " "le campus (1 mail par semaine)" -#: apps/wei/models.py:214 templates/wei/weimembership_form.html:77 +#: apps/wei/models.py:216 templates/wei/weimembership_form.html:77 msgid "" "Register on the mailing list to stay informed of the sport events of the " "campus (1 mail/week)" @@ -1342,7 +1351,7 @@ msgstr "" "S'inscrire sur la liste de diffusion pour rester informé des actualités " "sportives sur le campus (1 mail par semaine)" -#: apps/wei/models.py:219 templates/wei/weimembership_form.html:80 +#: apps/wei/models.py:221 templates/wei/weimembership_form.html:80 msgid "" "Register on the mailing list to stay informed of the art events of the " "campus (1 mail/week)" @@ -1350,19 +1359,19 @@ msgstr "" "S'inscrire sur la liste de diffusion pour rester informé des actualités " "artistiques sur le campus (1 mail par semaine)" -#: apps/wei/models.py:224 templates/wei/weimembership_form.html:56 +#: apps/wei/models.py:226 templates/wei/weimembership_form.html:56 msgid "first year" msgstr "première année" -#: apps/wei/models.py:225 +#: apps/wei/models.py:227 msgid "Tells if the user is new in the school." msgstr "Indique si l'utilisateur est nouveau dans l'école." -#: apps/wei/models.py:230 +#: apps/wei/models.py:232 msgid "registration information" msgstr "informations sur l'inscription" -#: apps/wei/models.py:231 +#: apps/wei/models.py:233 msgid "" "Information about the registration (buses for old members, survey fot the " "new members), encoded in JSON" @@ -1370,27 +1379,27 @@ msgstr "" "Informations sur l'inscription (bus pour les 2A+, questionnaire pour les " "1A), encodées en JSON" -#: apps/wei/models.py:262 +#: apps/wei/models.py:264 msgid "WEI User" msgstr "Participant au WEI" -#: apps/wei/models.py:263 +#: apps/wei/models.py:265 msgid "WEI Users" msgstr "Participants au WEI" -#: apps/wei/models.py:283 +#: apps/wei/models.py:285 msgid "team" msgstr "équipe" -#: apps/wei/models.py:293 +#: apps/wei/models.py:295 msgid "WEI registration" msgstr "inscription au WEI" -#: apps/wei/models.py:297 +#: apps/wei/models.py:299 msgid "WEI membership" msgstr "adhésion au WEI" -#: apps/wei/models.py:298 +#: apps/wei/models.py:300 msgid "WEI memberships" msgstr "adhésions au WEI" @@ -1462,15 +1471,15 @@ msgstr "Cet utilisateur n'a pas donné son chèque de caution." msgid "Survey WEI" msgstr "Questionnaire WEI" -#: note_kfet/settings/base.py:154 +#: note_kfet/settings/base.py:155 msgid "German" msgstr "Allemand" -#: note_kfet/settings/base.py:155 +#: note_kfet/settings/base.py:156 msgid "English" msgstr "Anglais" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:157 msgid "French" msgstr "Français" From 4c29d855d288ea091f27031083fe1afc5f40a859 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 20:07:45 +0200 Subject: [PATCH 14/77] Fix RolePermissions merge --- apps/permission/backends.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/permission/backends.py b/apps/permission/backends.py index 9dc69d3d..99f1b2da 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -37,17 +37,17 @@ class PermissionBackend(ModelBackend): return Permission.objects.none() qs = Permission.objects.annotate( - club=F("role__role__membership__club"), - membership=F("role__role__membership"), + club=F("role__membership__club"), + membership=F("role__membership"), ).filter( ( Q( - role__role__membership__date_start__lte=timezone.now().today(), - role__role__membership__date_end__gte=timezone.now().today(), + role__membership__date_start__lte=timezone.now().today(), + role__membership__date_end__gte=timezone.now().today(), ) | Q(permanent=True) ) - & Q(role__role__membership__user=user) + & Q(role__membership__user=user) & Q(type=t) & Q(mask__rank__lte=get_current_session().get("permission_mask", 0)) ) From ab5d5a6e94edf44781709c777ab6a49285ddf288 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 25 Jul 2020 20:15:26 +0200 Subject: [PATCH 15/77] Display the associated club of the role in the permission page --- templates/permission/all_rights.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/permission/all_rights.html b/templates/permission/all_rights.html index 293e3386..80337134 100644 --- a/templates/permission/all_rights.html +++ b/templates/permission/all_rights.html @@ -15,14 +15,14 @@ {% regroup active_memberships by roles as memberships_per_role %} {% for role in roles %}
  • - {{ role }} {% if role.weirole %}(Pour le WEI){% endif %} + {{ role }} {% if role.weirole %}(Pour le WEI){% endif %} {% if role.for_club %}(Pour le club {{ role.for_club }} uniquement){% endif %} {% if role.clubs %}
    {% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }}
    {% endif %}
    +
    +
    From b49db390805674949a3a6b6bcd90600ac7ebea1f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Wed, 29 Jul 2020 23:00:57 +0200 Subject: [PATCH 43/77] Update Nginx conf: redirect automatically requests to the right domain --- .../roles/4-nginx/templates/nginx_note.conf | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/ansible/roles/4-nginx/templates/nginx_note.conf b/ansible/roles/4-nginx/templates/nginx_note.conf index 8a84c03c..9be2d980 100644 --- a/ansible/roles/4-nginx/templates/nginx_note.conf +++ b/ansible/roles/4-nginx/templates/nginx_note.conf @@ -3,8 +3,36 @@ upstream note{ server unix:///var/www/note_kfet/note_kfet.sock; # file socket } +# Redirect HTTP to nk20 HTTPS +server { + listen 80 default_server; + listen [::]:80 default_server; + + location / { + return 301 https://nk20-beta.crans.org$request_uri; + } +} + +# Redirect all HTTPS to nk20 HTTPS +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + location / { + return 301 https://nk20-beta.crans.org$request_uri; + } + + ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +} + # configuration of the server server { + listen 443 ssl; + listen [::]:443 ssl; + # the port your site will be served on # the domain name it will serve for server_name nk20-beta.crans.org; # substitute your machine's IP address or FQDN @@ -28,23 +56,8 @@ server { include /var/www/note_kfet/uwsgi_params; # the uwsgi_params file you installed } - listen 443 ssl; ssl_certificate /etc/letsencrypt/live/nk20-beta.crans.org/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/nk20-beta.crans.org/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - -} - -server { - if ($host = nk20-beta.crans.org) { - return 301 https://$host$request_uri; - } - - - listen 80; - server_name nk20-beta.crans.org; - return 404; - - } From fb775de923969e1f5add47f2a77021e141c90b6b Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 12:50:48 +0200 Subject: [PATCH 44/77] Add backdoor to login as other users (in debug mode only) --- apps/member/hashers.py | 34 +++++++++++++++++++++++++++++++++- apps/member/views.py | 4 ++++ apps/permission/backends.py | 3 +-- apps/permission/decorators.py | 5 +++++ apps/permission/signals.py | 6 +++++- note_kfet/settings/__init__.py | 8 +++++--- 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/apps/member/hashers.py b/apps/member/hashers.py index 0c5d010b..9ebed95b 100644 --- a/apps/member/hashers.py +++ b/apps/member/hashers.py @@ -3,9 +3,12 @@ import hashlib -from django.contrib.auth.hashers import PBKDF2PasswordHasher +from django.conf import settings +from django.contrib.auth.hashers import PBKDF2PasswordHasher, BasePasswordHasher from django.utils.crypto import constant_time_compare +from note_kfet.middlewares import get_current_authenticated_user, get_current_session + class CustomNK15Hasher(PBKDF2PasswordHasher): """ @@ -20,8 +23,37 @@ class CustomNK15Hasher(PBKDF2PasswordHasher): """ algorithm = "custom_nk15" + def must_update(self, encoded): + if settings.DEBUG: + current_user = get_current_authenticated_user() + if current_user is not None and current_user.is_superuser: + return False + return True + def verify(self, password, encoded): + if settings.DEBUG: + current_user = get_current_authenticated_user() + if current_user is not None and current_user.is_superuser\ + and get_current_session().get("permission_mask", -1) >= 42: + return True + if '|' in encoded: salt, db_hashed_pass = encoded.split('$')[2].split('|') return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass) return super().verify(password, encoded) + + +class DebugSuperuserBackdoor(PBKDF2PasswordHasher): + """ + In debug mode and during the beta, superusers can login into other accounts for tests. + """ + def must_update(self, encoded): + return False + + def verify(self, password, encoded): + if settings.DEBUG: + current_user = get_current_authenticated_user() + if current_user is not None and current_user.is_superuser\ + and get_current_session().get("permission_mask", -1) >= 42: + return True + return super().verify(password, encoded) diff --git a/apps/member/views.py b/apps/member/views.py index 02cf69db..bc555acf 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from PIL import Image from django.conf import settings +from django.contrib.auth import logout from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.auth.views import LoginView @@ -21,6 +22,7 @@ from note.forms import ImageForm from note.models import Alias, NoteUser from note.models.transactions import Transaction, SpecialTransaction from note.tables import HistoryTable, AliasTable +from note_kfet.middlewares import _set_current_user_and_ip from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin @@ -38,6 +40,8 @@ class CustomLoginView(LoginView): form_class = CustomAuthenticationForm def form_valid(self, form): + logout(self.request) + _set_current_user_and_ip(form.get_user(), self.request.session, None) self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank return super().form_valid(form) diff --git a/apps/permission/backends.py b/apps/permission/backends.py index c1291a2f..c29419eb 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -1,7 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.conf import settings from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, AnonymousUser from django.contrib.contenttypes.models import ContentType @@ -137,7 +136,7 @@ class PermissionBackend(ModelBackend): if sess is not None and sess.session_key is None: return False - if user_obj.is_superuser and get_current_session().get("permission_mask", -1) >= 42: + if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42: return True if obj is None: diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py index f144935a..8963cb0b 100644 --- a/apps/permission/decorators.py +++ b/apps/permission/decorators.py @@ -4,6 +4,7 @@ from functools import lru_cache from time import time +from django.conf import settings from django.contrib.sessions.models import Session from note_kfet.middlewares import get_current_session @@ -32,6 +33,10 @@ def memoize(f): sess_funs = new_sess_funs def func(*args, **kwargs): + if settings.DEBUG: + # Don't memoize in DEBUG mode + return f(*args, **kwargs) + nonlocal last_collect if time() - last_collect > 60: diff --git a/apps/permission/signals.py b/apps/permission/signals.py index cac0a8a0..9b4bb161 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -1,6 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - +from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.utils.translation import gettext_lazy as _ from note_kfet.middlewares import get_current_authenticated_user @@ -50,6 +50,10 @@ def pre_save_object(sender, instance, **kwargs): # In the other case, we check if he/she has the right to change one field previous = qs.get() + + if isinstance(instance, User) and instance.last_login != previous.last_login: + pass #return + for field in instance._meta.fields: field_name = field.name old_value = getattr(previous, field.name) diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 73aae469..ce691cc9 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -57,6 +57,8 @@ if "cas_server" in INSTALLED_APPS: if "logs" in INSTALLED_APPS: MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) -if "debug_toolbar" in INSTALLED_APPS: - MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") - INTERNAL_IPS = ['127.0.0.1'] +if DEBUG: + PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor'] + if "debug_toolbar" in INSTALLED_APPS: + MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") + INTERNAL_IPS = ['127.0.0.1'] From 0c0aed02343bea958929f0ec569dfdc45d70cd41 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 13:10:03 +0200 Subject: [PATCH 45/77] :bug: Force delete didn't work as well when trying to check add permissions --- apps/permission/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/permission/models.py b/apps/permission/models.py index 12db9135..79148fe4 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -44,7 +44,9 @@ class InstancedPermission: else: oldpk = obj.pk # Ensure previous models are deleted - self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete() + for o in self.model.model_class().objects.filter(pk=obj.pk).all(): + o._force_delete = True + Model.delete(o) # Force insertion, no data verification, no trigger obj._force_save = True Model.save(obj, force_insert=True) From e63219f7ad2ba23bbc7a099386b35cfc14122bb9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 14:58:18 +0200 Subject: [PATCH 46/77] Force delete some objects --- apps/permission/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/permission/signals.py b/apps/permission/signals.py index 9b4bb161..167a22b0 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -85,7 +85,8 @@ def pre_delete_object(instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return - if hasattr(instance, "_force_delete"): + if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0: + # Don't check permissions on force-deleted objects return user = get_current_authenticated_user() From 9361f3f2f01db60d99289418765ca2b590df6d2e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 15:07:30 +0200 Subject: [PATCH 47/77] Aliases should load really faster --- apps/api/viewsets.py | 12 ++++++++---- apps/note/api/views.py | 6 ++---- apps/permission/fixtures/initial.json | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py index f7532beb..6e0cb6b8 100644 --- a/apps/api/viewsets.py +++ b/apps/api/viewsets.py @@ -14,9 +14,11 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() + self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() + + def get_queryset(self): user = get_current_authenticated_user() - self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view")) + return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")) class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): @@ -26,6 +28,8 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() + self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() + + def get_queryset(self): user = get_current_authenticated_user() - self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view")) + return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 22470461..8a1616ce 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -106,10 +106,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): queryset = super().get_queryset() alias = self.request.query_params.get("alias", ".*") - queryset = queryset.filter( - Q(name__regex="^" + alias) - | Q(normalized_name__regex="^" + Alias.normalize(alias)) - | Q(normalized_name__regex="^" + alias.lower())).order_by('name') + queryset = queryset.filter(normalized_name__iregex="^" + Alias.normalize(alias))\ + .order_by('name').prefetch_related('note') return queryset diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 0391c095..c95776c5 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -111,7 +111,7 @@ "note", "alias" ], - "query": "[\"AND\", [\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__memberships__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}], {\"note__is_active\": true}]", + "query": "[\"AND\", [\"OR\", {\"note__noteuser__user__memberships__club__name\": \"Kfet\", \"note__noteuser__user__memberships__date_start__lte\": [\"today\"], \"note__noteuser__user__memberships__date_end__gte\": [\"today\"]}, {\"note__noteclub__isnull\": false}], {\"note__is_active\": true}]", "type": "view", "mask": 1, "field": "", @@ -319,7 +319,7 @@ "note", "note" ], - "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__memberships__club\": [\"club\"]}], [\"all\"]]}]", + "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"noteuser__user__memberships__club\": [\"club\"], \"noteuser__user__memberships__date_start__lte\": [\"today\"], \"noteuser__user__memberships__date_end__gte\": [\"today\"]}]", "type": "view", "mask": 2, "field": "", From 484560fe4be024cbbab117bd368fbf038bbf0ef9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 15:14:13 +0200 Subject: [PATCH 48/77] Fix emitter button --- static/js/base.js | 3 --- static/js/transfer.js | 19 ++++++++++++------- templates/note/transaction_form.html | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/static/js/base.js b/static/js/base.js index b03c1359..8bd6b8ca 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -130,7 +130,6 @@ function displayNote(note, alias, user_note_field = null, profile_pic_field = nu if (profile_pic_field != null) { $("#" + profile_pic_field).attr('src', img); $("#" + profile_pic_field).click(function () { - console.log(note); if (note.resourcetype === "NoteUser") { document.location.href = "/accounts/user/" + note.user; } else if (note.resourcetype === "NoteClub") { @@ -223,9 +222,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr // When the user type "Enter", the first alias is clicked field.keypress(function (event) { - console.log(notes); if (event.originalEvent.charCode === 13 && notes.length > 0) { - console.log(42); let li_obj = field.parent().find("ul li").first(); displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); li_obj.trigger("click"); diff --git a/static/js/transfer.js b/static/js/transfer.js index 97dad879..ed651640 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -7,7 +7,7 @@ function refreshHistory() { $("#history").load("/note/transfer/ #history"); } -function reset() { +function reset(refresh=true) { sources_notes_display.length = 0; sources.length = 0; dests_notes_display.length = 0; @@ -21,8 +21,10 @@ function reset() { $("#bank").val(""); $("#user_note").val(""); $("#profile_pic").attr("src", "/media/pic/default.png"); - refreshBalance(); - refreshHistory(); + if (refresh) { + refreshBalance(); + refreshHistory(); + } } $(document).ready(function() { @@ -138,15 +140,18 @@ $(document).ready(function() { $("#source_me").click(function() { // Shortcut to set the current user as the only emitter - reset(); + reset(false); let source_note = $("#source_note"); source_note.focus(); - source_note.val(username); + source_note.val(""); let event = jQuery.Event("keyup"); - event.originalEvent = {charCode: 0}; + event.originalEvent = {charCode: 97}; + source_note.trigger(event); + source_note.val(username); + event = jQuery.Event("keyup"); + event.originalEvent = {charCode: 97}; source_note.trigger(event); - console.log(sources.length); let fill_note = function() { if (sources.length === 0) { setTimeout(fill_note, 100); diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index 7757bad1..fd3e5406 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -56,9 +56,9 @@ SPDX-License-Identifier: GPL-2.0-or-later

    - +
    From 9d8c588b78aa27a5ffd3a8a8646174c986f6de10 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 15:49:59 +0200 Subject: [PATCH 49/77] Buttons list didn't work as well --- apps/note/tables.py | 2 - apps/note/views.py | 14 ++++ locale/de/LC_MESSAGES/django.po | 4 - locale/fr/LC_MESSAGES/django.po | 4 - static/js/alias.js | 4 +- templates/activity/activity_detail.html | 2 +- templates/activity/activity_entry.html | 2 +- templates/member/club_list.html | 2 +- templates/member/user_list.html | 2 +- templates/note/transactiontemplate_list.html | 77 ++++++-------------- templates/registration/future_user_list.html | 2 +- 11 files changed, 42 insertions(+), 73 deletions(-) diff --git a/apps/note/tables.py b/apps/note/tables.py index a1385abc..cbfd2424 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -131,12 +131,10 @@ class ButtonTable(tables.Table): row_attrs = { 'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'), 'id': lambda record: "row-" + str(record.pk), - 'data-href': lambda record: record.pk } model = TransactionTemplate exclude = ('id',) - order_by = ('type', '-display', 'destination__name', 'name',) edit = tables.LinkColumn('note:template_update', args=[A('pk')], diff --git a/apps/note/views.py b/apps/note/views.py index b62b0ae1..e856eec0 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -5,6 +5,7 @@ import json from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, UpdateView from django_tables2 import SingleTableView @@ -72,6 +73,19 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing model = TransactionTemplate table_class = ButtonTable + def get_queryset(self, **kwargs): + """ + Filter the user list with the given pattern. + """ + qs = super().get_queryset().distinct() + if "search" in self.request.GET: + pattern = self.request.GET["search"] + qs = qs.filter(Q(name__iregex="^" + pattern) | Q(destination__club__name__iregex="^" + pattern)) + + qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name') + + return qs + class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 8618d9e5..fffde96a 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -1784,10 +1784,6 @@ msgstr "" msgid "Name of the button..." msgstr "" -#: templates/note/transactiontemplate_list.html:16 -msgid "Display visible buttons only" -msgstr "" - #: templates/note/transactiontemplate_list.html:21 msgid "New button" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 16548d3b..2f499334 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -1846,10 +1846,6 @@ msgstr "Chercher un bouton" msgid "Name of the button..." msgstr "Nom du bouton ..." -#: templates/note/transactiontemplate_list.html:16 -msgid "Display visible buttons only" -msgstr "N'afficher que les boutons visibles uniquement" - #: templates/note/transactiontemplate_list.html:21 msgid "New button" msgstr "Nouveau bouton" diff --git a/static/js/alias.js b/static/js/alias.js index 267410da..cdb21b2b 100644 --- a/static/js/alias.js +++ b/static/js/alias.js @@ -13,7 +13,7 @@ "note": note_id } ).done(function(){ - $("#alias_table").load(location.href+ " #alias_table"); + $("#alias_table").load(location.pathname+ " #alias_table"); addMsg("Alias ajouté","success"); }) .fail(function(xhr, textStatus, error){ @@ -29,7 +29,7 @@ }) .done(function(){ addMsg('Alias supprimé','success'); - $("#alias_table").load(location.href + " #alias_table"); + $("#alias_table").load(location.pathname + " #alias_table"); }) .fail(function(xhr,textStatus, error){ errMsg(xhr.responseJSON); diff --git a/templates/activity/activity_detail.html b/templates/activity/activity_detail.html index 3a3de8d0..a955d194 100644 --- a/templates/activity/activity_detail.html +++ b/templates/activity/activity_detail.html @@ -92,7 +92,7 @@ }) .done(function() { addMsg('Invité supprimé','success'); - $("#guests_table").load(location.href + " #guests_table"); + $("#guests_table").load(location.pathname + " #guests_table"); }) .fail(function(xhr, textStatus, error) { errMsg(xhr.responseJSON); diff --git a/templates/activity/activity_entry.html b/templates/activity/activity_entry.html index a712228d..abf4eef8 100644 --- a/templates/activity/activity_entry.html +++ b/templates/activity/activity_entry.html @@ -51,7 +51,7 @@ if ((pattern === old_pattern || pattern === "") && !force) return; - $("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init); + $("#entry_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init); refreshBalance(); } diff --git a/templates/member/club_list.html b/templates/member/club_list.html index b43a186c..a69de324 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -36,7 +36,7 @@ function reloadTable() { let pattern = searchbar_obj.val(); - $("#club_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #club_table", init); + $("#club_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #club_table", init); } searchbar_obj.keyup(function() { diff --git a/templates/member/user_list.html b/templates/member/user_list.html index d7628882..bad37468 100644 --- a/templates/member/user_list.html +++ b/templates/member/user_list.html @@ -34,7 +34,7 @@ if (pattern === old_pattern || pattern === "") return; - $("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); + $("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); } searchbar_obj.keyup(function() { diff --git a/templates/note/transactiontemplate_list.html b/templates/note/transactiontemplate_list.html index 280f9faf..1d2342b9 100644 --- a/templates/note/transactiontemplate_list.html +++ b/templates/note/transactiontemplate_list.html @@ -9,14 +9,6 @@ {% trans "Search button" %} -
    -
    - -
    -

    {% trans "New button" %} @@ -36,66 +28,39 @@ {% endblock %} {% block extrajavascript %} - {% endblock %} diff --git a/templates/registration/future_user_list.html b/templates/registration/future_user_list.html index 1e10dcbb..c2c888fd 100644 --- a/templates/registration/future_user_list.html +++ b/templates/registration/future_user_list.html @@ -32,7 +32,7 @@ if (pattern === old_pattern || pattern === "") return; - $("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); + $("#user_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); $(".table-row").click(function() { window.document.location = $(this).data("href"); From e9cbc8e62346d7676f71c251fecfa63c48701cd8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 15:53:23 +0200 Subject: [PATCH 50/77] Fix linters --- apps/member/admin.py | 2 +- apps/member/forms.py | 1 - apps/member/hashers.py | 3 +-- apps/note/admin.py | 1 - apps/permission/admin.py | 2 +- apps/permission/backends.py | 3 +-- apps/permission/signals.py | 5 +---- apps/treasury/admin.py | 2 +- apps/wei/admin.py | 1 + apps/wei/models.py | 2 +- 10 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/member/admin.py b/apps/member/admin.py index 76edb762..d9075832 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -4,8 +4,8 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User - from note_kfet.admin import admin_site + from .forms import ProfileForm from .models import Club, Membership, Profile diff --git a/apps/member/forms.py b/apps/member/forms.py index 49537838..a31acb80 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -32,7 +32,6 @@ class UserForm(forms.ModelForm): fields = ('first_name', 'last_name', 'username', 'email',) - class ProfileForm(forms.ModelForm): """ A form for the extras field provided by the :model:`member.Profile` model. diff --git a/apps/member/hashers.py b/apps/member/hashers.py index 9ebed95b..f7e4342f 100644 --- a/apps/member/hashers.py +++ b/apps/member/hashers.py @@ -4,9 +4,8 @@ import hashlib from django.conf import settings -from django.contrib.auth.hashers import PBKDF2PasswordHasher, BasePasswordHasher +from django.contrib.auth.hashers import PBKDF2PasswordHasher from django.utils.crypto import constant_time_compare - from note_kfet.middlewares import get_current_authenticated_user, get_current_session diff --git a/apps/note/admin.py b/apps/note/admin.py index 96823d2b..d2750bd8 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -5,7 +5,6 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ from polymorphic.admin import PolymorphicChildModelAdmin, \ PolymorphicChildModelFilter, PolymorphicParentModelAdmin - from note_kfet.admin import admin_site from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser diff --git a/apps/permission/admin.py b/apps/permission/admin.py index 7d189c94..385cf198 100644 --- a/apps/permission/admin.py +++ b/apps/permission/admin.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-lateré from django.contrib import admin - from note_kfet.admin import admin_site + from .models import Permission, PermissionMask, Role diff --git a/apps/permission/backends.py b/apps/permission/backends.py index c29419eb..c5a43b1e 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -36,7 +36,7 @@ class PermissionBackend(ModelBackend): return Permission.objects.none() memberships = Membership.objects.filter(user=user).all() - + perms = [] for membership in memberships: @@ -49,7 +49,6 @@ class PermissionBackend(ModelBackend): perms.append(perm) return perms - @staticmethod def permissions(user, model, type): """ diff --git a/apps/permission/signals.py b/apps/permission/signals.py index 167a22b0..ef21c36e 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -1,6 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.contrib.auth.models import User + from django.core.exceptions import PermissionDenied from django.utils.translation import gettext_lazy as _ from note_kfet.middlewares import get_current_authenticated_user @@ -51,9 +51,6 @@ def pre_save_object(sender, instance, **kwargs): # In the other case, we check if he/she has the right to change one field previous = qs.get() - if isinstance(instance, User) and instance.last_login != previous.last_login: - pass #return - for field in instance._meta.fields: field_name = field.name old_value = getattr(previous, field.name) diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py index e1b597b9..464816db 100644 --- a/apps/treasury/admin.py +++ b/apps/treasury/admin.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-lateré from django.contrib import admin - from note_kfet.admin import admin_site + from .models import RemittanceType, Remittance, SogeCredit diff --git a/apps/wei/admin.py b/apps/wei/admin.py index a3e1d546..f928a313 100644 --- a/apps/wei/admin.py +++ b/apps/wei/admin.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from note_kfet.admin import admin_site + from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam admin_site.register(WEIClub) diff --git a/apps/wei/models.py b/apps/wei/models.py index 1b19a60a..df353338 100644 --- a/apps/wei/models.py +++ b/apps/wei/models.py @@ -202,7 +202,7 @@ class WEIRegistration(models.Model): clothing_size = models.CharField( max_length=4, choices=( - ('XS',"XS"), + ('XS', "XS"), ('S', "S"), ('M', "M"), ('L', "L"), From c14d37eaeb76b4e02edac2b1a6f8fee37596bd43 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 16:06:21 +0200 Subject: [PATCH 51/77] Fix deletion of consumptions in double consumption mode --- static/js/base.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/static/js/base.js b/static/js/base.js index 8bd6b8ca..fccb5b4f 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -95,6 +95,8 @@ function li(id, text, extra_css) { * @param note The concerned note. */ function displayStyle(note) { + if (!note) + return ""; let balance = note.balance; var css = ""; if (balance < -5000) @@ -274,7 +276,6 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr field.attr('data-original-title', aliases_matched_html).tooltip('show'); consumers.results.forEach(function (consumer) { - let note = consumer.note; let consumer_obj = $("#" + alias_prefix + "_" + consumer.id); consumer_obj.hover(function () { displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field) @@ -282,8 +283,8 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr consumer_obj.click(function () { var disp = null; notes_display.forEach(function (d) { - // We compare the note ids - if (d.id === note.id) { + // We compare the alias ids + if (d.id === consumer.id) { d.quantity += 1; disp = d; } @@ -293,7 +294,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr disp = { name: consumer.name, id: consumer.id, - note: note, + note: consumer.note, quantity: 1 }; notes_display.push(disp); From aa66361ac71fe39ddb42e75c9aca1a24f8b6bfe2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 16:36:44 +0200 Subject: [PATCH 52/77] Update permissions to create clubs. For now, only superusers can edit the roles of a user. --- apps/member/views.py | 9 ++-- apps/permission/fixtures/initial.json | 69 ++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/apps/member/views.py b/apps/member/views.py index bc555acf..f106a8e5 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -26,7 +26,6 @@ from note_kfet.middlewares import _set_current_user_and_ip from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin -from wei.models import WEIClub from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm from .models import Club, Membership @@ -432,7 +431,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ .get(pk=self.kwargs["club_pk"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not isinstance(club, WEIClub)) + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) & (Q(for_club__isnull=True) | Q(for_club=club))).all() form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() @@ -453,7 +452,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): user = old_membership.user form.fields['user'].initial = user form.fields['user'].disabled = True - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not isinstance(club, WEIClub)) + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) & (Q(for_club__isnull=True) | Q(for_club=club))).all() form.fields['roles'].initial = old_membership.roles.all() form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1) @@ -647,10 +646,10 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): del form.fields['bank'] club = self.object.club - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not isinstance(club, WEIClub)) + form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) & (Q(for_club__isnull=True) | Q(for_club=club))).all() return form def get_success_url(self): - return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) + return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index c95776c5..572e0716 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -801,7 +801,7 @@ ], "query": "{\"club\": [\"club\"]}", "type": "view", - "mask": 1, + "mask": 3, "field": "", "permanent": false, "description": "View club's memberships" @@ -833,7 +833,7 @@ ], "query": "{\"club\": [\"club\"]}", "type": "change", - "mask": 2, + "mask": 3, "field": "roles", "permanent": false, "description": "Update user roles" @@ -2234,6 +2234,22 @@ { "model": "permission.permission", "pk": 139, + "fields": { + "model": [ + "note", + "noteclub" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "permanent": false, + "description": "Create any noteclub" + } + }, + { + "model": "permission.permission", + "pk": 140, "fields": { "model": [ "member", @@ -2247,6 +2263,38 @@ "description": "Create any club" } }, + { + "model": "permission.permission", + "pk": 141, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"memberships__club\": [\"club\"], \"memberships__date__start__lte\": [\"today\"], \"memberships__date__end__gte\": [\"today\"]}", + "type": "view", + "mask": 3, + "field": "", + "permanent": false, + "description": "View members of our club" + } + }, + { + "model": "permission.permission", + "pk": 142, + "fields": { + "model": [ + "note", + "noteclub" + ], + "query": "{\"club\": [\"club\"]}", + "type": "view", + "mask": 2, + "field": "", + "permanent": false, + "description": "View club note" + } + }, { "model": "permission.role", "pk": 1, @@ -2323,7 +2371,8 @@ "permissions": [ 22, 47, - 49 + 49, + 140 ] } }, @@ -2335,8 +2384,9 @@ "name": "Pr\u00e9sident\u00b7e de club", "permissions": [ 50, - 51, - 62 + 62, + 141, + 142 ] } }, @@ -2356,7 +2406,9 @@ 61, 62, 127, - 133 + 133, + 141, + 142 ] } }, @@ -2564,7 +2616,10 @@ 136, 137, 138, - 139 + 139, + 140, + 141, + 142 ] } }, From 9da8d492236a5e8ece4968834b5ef202882c15e1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 16:41:41 +0200 Subject: [PATCH 53/77] Remove ... when invalidating transaction --- static/js/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/base.js b/static/js/base.js index fccb5b4f..77057103 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -343,7 +343,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr // When a validate button is clicked, we switch the validation status function de_validate(id, validated) { let invalidity_reason = $("#invalidity_reason_" + id).val(); - $("#validate_" + id).html("⟳ ..."); + $("#validate_" + id).html(""); // Perform a PATCH request to the API in order to update the transaction // If the user has insufficient rights, an error message will appear From a5e50e5de6a3d9434878a22566e77f9f8378a111 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 16:48:34 +0200 Subject: [PATCH 54/77] Display true note name next to the alias, whenever the user has low permissions --- apps/note/api/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index c6681ee7..bcf0bdf5 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -119,7 +119,7 @@ class ConsumerSerializer(serializers.ModelSerializer): # If the user has no right to see the note, then we only display the note identifier if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note): return NotePolymorphicSerializer().to_representation(obj.note) - return dict(id=obj.note.id) + return dict(id=obj.note.id, name=str(obj.note)) def get_email_confirmed(self, obj): if isinstance(obj.note, NoteUser): From ae629b55ad0b56f82733472a179b44dbd5235426 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 17:30:21 +0200 Subject: [PATCH 55/77] Add HTML titles --- apps/activity/views.py | 12 +- apps/member/views.py | 21 +- apps/note/views.py | 7 +- apps/permission/views.py | 1 + apps/registration/views.py | 8 +- apps/treasury/views.py | 11 +- apps/wei/views.py | 23 +- locale/de/LC_MESSAGES/django.po | 404 ++++++++++++------ locale/fr/LC_MESSAGES/django.po | 408 +++++++++++++------ templates/member/club_list.html | 3 - templates/note/transactiontemplate_list.html | 3 - templates/wei/weiclub_list.html | 3 - 12 files changed, 639 insertions(+), 265 deletions(-) diff --git a/apps/activity/views.py b/apps/activity/views.py index 7bc8def5..cac7f183 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -23,6 +23,7 @@ from .tables import ActivityTable, GuestTable, EntryTable class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Activity form_class = ActivityForm + extra_context = {"title": _("Create new activity")} def form_valid(self, form): form.instance.creater = self.request.user @@ -37,6 +38,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView model = Activity table_class = ActivityTable ordering = ('-date_start',) + extra_context = {"title": _("Activities")} def get_queryset(self): return super().get_queryset().distinct() @@ -44,8 +46,6 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['title'] = _("Activities") - upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) context['upcoming'] = ActivityTable( data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")), @@ -58,6 +58,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = Activity context_object_name = "activity" + extra_context = {"title": _("Activity detail")} def get_context_data(self, **kwargs): context = super().get_context_data() @@ -74,6 +75,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Activity form_class = ActivityForm + extra_context = {"title": _("Update activity")} def get_success_url(self, **kwargs): return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) @@ -84,6 +86,12 @@ class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): form_class = GuestForm template_name = "activity/activity_invite.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + activity = context["form"].activity + context["title"] = _('Invite guest to the activity "{}"').format(activity.name) + return context + def get_form(self, form_class=None): form = super().get_form(form_class) form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ diff --git a/apps/member/views.py b/apps/member/views.py index f106a8e5..bc0ed3dd 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -53,6 +53,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form_class = UserForm template_name = 'member/profile_update.html' context_object_name = 'user_object' + extra_context = {"title": _("Update Profile")} profile_form = ProfileForm @@ -68,7 +69,6 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form.fields['email'].help_text = _("This address must be valid.") context['profile_form'] = self.profile_form(instance=context['user_object'].profile) - context['title'] = _("Update Profile") return context def form_valid(self, form): @@ -123,6 +123,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = User context_object_name = "user_object" template_name = "member/profile_detail.html" + extra_context = {"title": _("Profile detail")} def get_queryset(self, **kwargs): """ @@ -156,6 +157,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = User table_class = UserTable template_name = 'member/user_list.html' + extra_context = {"title": _("Search user")} def get_queryset(self, **kwargs): """ @@ -181,13 +183,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): return qs[:20] - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - context["title"] = _("Search user") - - return context - class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ @@ -196,6 +191,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' + extra_context = {"title": _("Note aliases")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -209,6 +205,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det Update profile picture of the user note. """ form_class = ImageForm + extra_context = {"title": _("Update note picture")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -266,6 +263,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): """ model = Token template_name = "member/manage_auth_tokens.html" + extra_context = {"title": _("Manage auth token")} def get(self, request, *args, **kwargs): if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): @@ -293,6 +291,7 @@ class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Club form_class = ClubForm success_url = reverse_lazy('member:club_list') + extra_context = {"title": _("Create new club")} def form_valid(self, form): return super().form_valid(form) @@ -304,6 +303,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ model = Club table_class = ClubTable + extra_context = {"title": _("Search club")} def get_queryset(self, **kwargs): """ @@ -328,6 +328,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ model = Club context_object_name = "club" + extra_context = {"title": _("Club detail")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -372,6 +373,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): model = Club template_name = 'member/club_alias.html' context_object_name = 'club' + extra_context = {"title": _("Note aliases")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -388,6 +390,7 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): context_object_name = "club" form_class = ClubForm template_name = "member/club_form.html" + extra_context = {"title": _("Update club")} def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) @@ -421,6 +424,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): model = Membership form_class = MembershipForm template_name = 'member/add_members.html' + extra_context = {"title": _("Add new member to the club")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -627,6 +631,7 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = Membership form_class = MembershipForm template_name = 'member/add_members.html' + extra_context = {"title": _("Manage roles of an user in the club")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/apps/note/views.py b/apps/note/views.py index e856eec0..61b86e92 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -30,6 +30,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl model = Transaction # Transaction history table table_class = HistoryTable + extra_context = {"title": _("Transfer money")} def get_queryset(self, **kwargs): return super().get_queryset(**kwargs).order_by("-created_at").all()[:20] @@ -39,7 +40,6 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['title'] = _('Transfer money') context['amount_widget'] = AmountInput(attrs={"id": "amount"}) context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk @@ -64,6 +64,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr model = TransactionTemplate form_class = TransactionTemplateForm success_url = reverse_lazy('note:template_list') + extra_context = {"title": _("Create new button")} class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): @@ -72,6 +73,7 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing """ model = TransactionTemplate table_class = ButtonTable + extra_context = {"title": _("Search button")} def get_queryset(self, **kwargs): """ @@ -94,6 +96,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up model = TransactionTemplate form_class = TransactionTemplateForm success_url = reverse_lazy('note:template_list') + extra_context = {"title": _("Update button")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -130,6 +133,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ model = Transaction template_name = "note/conso_form.html" + extra_context = {"title": _("Consumptions")} # Transaction history table table_class = HistoryTable @@ -151,7 +155,6 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter( PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view") ).order_by('name').all() - context['title'] = _("Consumptions") context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk # select2 compatibility diff --git a/apps/permission/views.py b/apps/permission/views.py index f3ed5641..83deddac 100644 --- a/apps/permission/views.py +++ b/apps/permission/views.py @@ -41,6 +41,7 @@ class ProtectQuerysetMixin: class RightsView(TemplateView): template_name = "permission/all_rights.html" + extra_context = {"title": _("Rights")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/apps/registration/views.py b/apps/registration/views.py index bca0d217..ec6bfd1c 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -35,6 +35,7 @@ class UserCreateView(CreateView): form_class = SignUpForm template_name = 'registration/signup.html' second_form = ProfileForm + extra_context = {"title": _("Register new user")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -78,6 +79,7 @@ class UserValidateView(TemplateView): """ title = _("Email validation") template_name = 'registration/email_validation_complete.html' + extra_context = {"title": _("Validate a registration")} def get(self, *args, **kwargs): """ @@ -133,7 +135,7 @@ class UserValidationEmailSentView(TemplateView): Display the information that the validation link has been sent. """ template_name = 'registration/email_validation_email_sent.html' - title = _('Email validation email sent') + extra_context = {"title": _('Email validation email sent')} class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView): @@ -141,6 +143,7 @@ class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, De Rensend the email validation link. """ model = User + extra_context = {"title": _("Resend email validation link")} def get(self, request, *args, **kwargs): user = self.get_object() @@ -158,6 +161,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi model = User table_class = FutureUserTable template_name = 'registration/future_user_list.html' + extra_context = {"title": _("Pre-registered users list")} def get_queryset(self, **kwargs): """ @@ -199,6 +203,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, form_class = ValidationForm context_object_name = "user_object" template_name = "registration/future_profile_detail.html" + extra_context = {"title": _("Registration detail")} def post(self, request, *args, **kwargs): form = self.get_form() @@ -355,6 +360,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View): """ Delete a pre-registered user. """ + extra_context = {"title": _("Invalidate pre-registration")} def get(self, request, *args, **kwargs): """ diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 1ab87f09..07d480aa 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -15,6 +15,7 @@ from django.http import HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, UpdateView, DetailView from django.views.generic.base import View, TemplateView from django.views.generic.edit import BaseFormView @@ -35,6 +36,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = Invoice form_class = InvoiceForm + extra_context = {"title": _("Create new invoice")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -77,6 +79,7 @@ class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView) """ model = Invoice table_class = InvoiceTable + extra_context = {"title": _("Invoices list")} class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): @@ -85,6 +88,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ model = Invoice form_class = InvoiceForm + extra_context = {"title": _("Update an invoice")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -198,6 +202,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView) """ model = Remittance form_class = RemittanceForm + extra_context = {"title": _("Create a new remittance")} def get_success_url(self): return reverse_lazy('treasury:remittance_list') @@ -218,6 +223,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): List existing Remittances """ template_name = "treasury/remittance_list.html" + extra_context = {"title": _("Remittances list")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -267,6 +273,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView) """ model = Remittance form_class = RemittanceForm + extra_context = {"title": _("Update a remittance")} def get_success_url(self): return reverse_lazy('treasury:remittance_list') @@ -289,9 +296,9 @@ class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, """ Attach a special transaction to a remittance """ - model = SpecialTransactionProxy form_class = LinkTransactionToRemittanceForm + extra_context = {"title": _("Attach a transaction to a remittance")} def get_success_url(self): return reverse_lazy('treasury:remittance_list') @@ -335,6 +342,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi """ model = SogeCredit table_class = SogeCreditTable + extra_context = {"title": _("List of credits from the Société générale")} def get_queryset(self, **kwargs): """ @@ -373,6 +381,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie """ model = SogeCredit form_class = Form + extra_context = {"title": _("Manage credits from the Société générale")} def form_valid(self, form): if "validate" in form.data: diff --git a/apps/wei/views.py b/apps/wei/views.py index be73370f..dde9fe78 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -52,6 +52,7 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): model = WEIClub table_class = WEITable ordering = '-year' + extra_context = {"title": _("Search WEI")} class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): @@ -60,6 +61,7 @@ class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = WEIClub form_class = WEIForm + extra_context = {"title": _("Create WEI")} def form_valid(self, form): form.instance.requires_membership = True @@ -79,6 +81,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ model = WEIClub context_object_name = "club" + extra_context = {"title": _("WEI Detail")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -173,6 +176,7 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi """ model = WEIMembership table_class = WEIMembershipTable + extra_context = {"title": _("View members of the WEI")} def dispatch(self, request, *args, **kwargs): self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) @@ -210,6 +214,7 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable """ model = WEIRegistration table_class = WEIRegistrationTable + extra_context = {"title": _("View registrations to the WEI")} def dispatch(self, request, *args, **kwargs): self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) @@ -246,6 +251,7 @@ class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): model = WEIClub context_object_name = "club" form_class = WEIForm + extra_context = {"title": _("Update the WEI")} def dispatch(self, request, *args, **kwargs): wei = self.get_object() @@ -266,6 +272,7 @@ class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = Bus form_class = BusForm + extra_context = {"title": _("Create new bus")} def dispatch(self, request, *args, **kwargs): wei = WEIClub.objects.get(pk=self.kwargs["pk"]) @@ -296,6 +303,7 @@ class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ model = Bus form_class = BusForm + extra_context = {"title": _("Update bus")} def dispatch(self, request, *args, **kwargs): wei = self.get_object().wei @@ -325,6 +333,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): Manage Bus """ model = Bus + extra_context = {"title": _("Manage bus")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -351,6 +360,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = BusTeam form_class = BusTeamForm + extra_context = {"title": _("Create new team")} def dispatch(self, request, *args, **kwargs): wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"]) @@ -382,6 +392,7 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ model = BusTeam form_class = BusTeamForm + extra_context = {"title": _("Update team")} def dispatch(self, request, *args, **kwargs): wei = self.get_object().bus.wei @@ -412,6 +423,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): Manage Bus team """ model = BusTeam + extra_context = {"title": _("Manage WEI team")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -433,6 +445,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = WEIRegistration form_class = WEIRegistrationForm + extra_context = {"title": _("Register first year student to the WEI")} def dispatch(self, request, *args, **kwargs): wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) @@ -487,6 +500,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ model = WEIRegistration form_class = WEIRegistrationForm + extra_context = {"title": _("Register old student to the WEI")} def dispatch(self, request, *args, **kwargs): wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) @@ -564,6 +578,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update """ model = WEIRegistration form_class = WEIRegistrationForm + extra_context = {"title": _("Update WEI Registration")} def get_queryset(self, **kwargs): return WEIRegistration.objects @@ -653,6 +668,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete Delete a non-validated WEI registration """ model = WEIRegistration + extra_context = {"title": _("Delete WEI registration")} def dispatch(self, request, *args, **kwargs): object = self.get_object() @@ -682,6 +698,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea """ model = WEIMembership form_class = WEIMembershipForm + extra_context = {"title": _("Validate WEI registration")} def dispatch(self, request, *args, **kwargs): wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei @@ -799,6 +816,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): model = WEIRegistration template_name = "wei/survey.html" survey = None + extra_context = {"title": _("Survey WEI")} def dispatch(self, request, *args, **kwargs): obj = self.get_object() @@ -836,7 +854,6 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["club"] = self.object.wei - context["title"] = _("Survey WEI") return context def form_valid(self, form): @@ -852,21 +869,21 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): class WEISurveyEndView(LoginRequiredMixin, TemplateView): template_name = "wei/survey_end.html" + extra_context = {"title": _("Survey WEI")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei - context["title"] = _("Survey WEI") return context class WEIClosedView(LoginRequiredMixin, TemplateView): template_name = "wei/survey_closed.html" + extra_context = {"title": _("Survey WEI")} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"]) - context["title"] = _("Survey WEI") return context diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index fffde96a..e4526352 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-29 22:54+0200\n" +"POT-Creation-Date: 2020-07-30 17:22+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,7 +46,7 @@ msgstr "" #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/note/models/notes.py:188 #: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 -#: apps/note/models/transactions.py:261 apps/permission/models.py:321 +#: apps/note/models/transactions.py:261 apps/permission/models.py:323 #: apps/wei/models.py:65 apps/wei/models.py:117 #: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -71,14 +71,14 @@ msgid "activity types" msgstr "" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:102 apps/permission/models.py:181 +#: apps/permission/models.py:104 apps/permission/models.py:183 #: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:156 +#: apps/note/models/transactions.py:66 apps/permission/models.py:158 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "" @@ -186,12 +186,12 @@ msgstr "" msgid "Type" msgstr "" -#: apps/activity/tables.py:77 apps/member/forms.py:105 +#: apps/activity/tables.py:77 apps/member/forms.py:104 #: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "" -#: apps/activity/tables.py:79 apps/member/forms.py:110 +#: apps/activity/tables.py:79 apps/member/forms.py:109 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" @@ -205,11 +205,27 @@ msgstr "" msgid "Balance" msgstr "" -#: apps/activity/views.py:47 templates/base.html:121 +#: apps/activity/views.py:26 +msgid "Create new activity" +msgstr "" + +#: apps/activity/views.py:41 templates/base.html:121 msgid "Activities" msgstr "" -#: apps/activity/views.py:163 +#: apps/activity/views.py:61 +msgid "Activity detail" +msgstr "" + +#: apps/activity/views.py:78 +msgid "Update activity" +msgstr "" + +#: apps/activity/views.py:92 +msgid "Invite guest to the activity \"{}\"" +msgstr "" + +#: apps/activity/views.py:171 msgid "Entry for activity \"{}\"" msgstr "" @@ -225,7 +241,7 @@ msgstr "" msgid "IP Address" msgstr "" -#: apps/logs/models.py:35 apps/permission/models.py:126 +#: apps/logs/models.py:35 apps/permission/models.py:128 msgid "model" msgstr "" @@ -245,12 +261,12 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 apps/note/tables.py:145 +#: apps/logs/models.py:61 apps/note/tables.py:143 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "" -#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:150 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:148 #: apps/wei/tables.py:65 msgid "delete" msgstr "" @@ -279,35 +295,35 @@ msgstr "" msgid "member" msgstr "" -#: apps/member/forms.py:59 apps/member/views.py:78 +#: apps/member/forms.py:58 apps/member/views.py:81 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/forms.py:84 apps/registration/forms.py:44 +#: apps/member/forms.py:83 apps/registration/forms.py:44 msgid "Inscription paid by Société Générale" msgstr "" -#: apps/member/forms.py:86 apps/registration/forms.py:46 +#: apps/member/forms.py:85 apps/registration/forms.py:46 msgid "Check this case is the Société Générale paid the inscription." msgstr "" -#: apps/member/forms.py:91 apps/registration/forms.py:51 +#: apps/member/forms.py:90 apps/registration/forms.py:51 msgid "Credit type" msgstr "" -#: apps/member/forms.py:92 apps/registration/forms.py:52 +#: apps/member/forms.py:91 apps/registration/forms.py:52 msgid "No credit" msgstr "" -#: apps/member/forms.py:94 +#: apps/member/forms.py:93 msgid "You can credit the note of the user." msgstr "" -#: apps/member/forms.py:98 apps/registration/forms.py:57 +#: apps/member/forms.py:97 apps/registration/forms.py:57 msgid "Credit amount" msgstr "" -#: apps/member/forms.py:115 apps/registration/forms.py:74 +#: apps/member/forms.py:114 apps/registration/forms.py:74 #: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "" @@ -509,7 +525,7 @@ msgstr "" msgid "fee" msgstr "" -#: apps/member/models.py:303 apps/member/views.py:528 apps/wei/views.py:770 +#: apps/member/models.py:303 apps/member/views.py:535 apps/wei/views.py:787 msgid "User is not a member of the parent club" msgstr "" @@ -518,7 +534,7 @@ msgstr "" msgid "The role {role} does not apply to the club {club}." msgstr "" -#: apps/member/models.py:321 apps/member/views.py:537 +#: apps/member/models.py:321 apps/member/views.py:544 msgid "User is already a member of the club" msgstr "" @@ -539,45 +555,85 @@ msgstr "" msgid "Renew" msgstr "" -#: apps/member/views.py:65 apps/registration/forms.py:23 -msgid "This address must be valid." -msgstr "" - -#: apps/member/views.py:68 templates/member/profile_info.html:47 +#: apps/member/views.py:56 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:130 msgid "Update Profile" msgstr "" -#: apps/member/views.py:184 +#: apps/member/views.py:69 apps/registration/forms.py:23 +msgid "This address must be valid." +msgstr "" + +#: apps/member/views.py:126 +msgid "Profile detail" +msgstr "" + +#: apps/member/views.py:160 msgid "Search user" msgstr "" -#: apps/member/views.py:523 apps/wei/views.py:761 +#: apps/member/views.py:194 apps/member/views.py:376 +msgid "Note aliases" +msgstr "" + +#: apps/member/views.py:208 +msgid "Update note picture" +msgstr "" + +#: apps/member/views.py:266 templates/member/profile_info.html:43 +msgid "Manage auth token" +msgstr "" + +#: apps/member/views.py:294 +msgid "Create new club" +msgstr "" + +#: apps/member/views.py:306 +msgid "Search club" +msgstr "" + +#: apps/member/views.py:331 +msgid "Club detail" +msgstr "" + +#: apps/member/views.py:393 +msgid "Update club" +msgstr "" + +#: apps/member/views.py:427 +msgid "Add new member to the club" +msgstr "" + +#: apps/member/views.py:530 apps/wei/views.py:778 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" -#: apps/member/views.py:541 +#: apps/member/views.py:548 msgid "The membership must start after {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:546 +#: apps/member/views.py:553 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:563 apps/member/views.py:565 apps/member/views.py:567 -#: apps/registration/views.py:290 apps/registration/views.py:292 -#: apps/registration/views.py:294 +#: apps/member/views.py:570 apps/member/views.py:572 apps/member/views.py:574 +#: apps/registration/views.py:295 apps/registration/views.py:297 +#: apps/registration/views.py:299 msgid "This field is required." msgstr "" -#: apps/note/admin.py:122 apps/note/models/transactions.py:106 +#: apps/member/views.py:634 +msgid "Manage roles of an user in the club" +msgstr "" + +#: apps/note/admin.py:121 apps/note/models/transactions.py:106 msgid "source" msgstr "" -#: apps/note/admin.py:130 apps/note/admin.py:172 +#: apps/note/admin.py:129 apps/note/admin.py:171 #: apps/note/models/transactions.py:55 apps/note/models/transactions.py:119 msgid "destination" msgstr "" @@ -814,113 +870,129 @@ msgstr "" msgid "No reason specified" msgstr "" -#: apps/note/tables.py:122 apps/note/tables.py:152 apps/wei/tables.py:66 +#: apps/note/tables.py:122 apps/note/tables.py:150 apps/wei/tables.py:66 #: templates/treasury/sogecredit_detail.html:59 #: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "" -#: apps/note/tables.py:147 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: apps/note/tables.py:145 apps/wei/tables.py:42 apps/wei/tables.py:43 #: templates/member/club_info.html:67 templates/note/conso_form.html:128 #: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 #: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 msgid "Edit" msgstr "" -#: apps/note/views.py:41 +#: apps/note/views.py:33 msgid "Transfer money" msgstr "" -#: apps/note/views.py:140 templates/base.html:94 -msgid "Consumptions" +#: apps/note/views.py:67 +msgid "Create new button" msgstr "" -#: apps/permission/models.py:81 -#, python-brace-format -msgid "Can {type} {model}.{field} in {query}" +#: apps/note/views.py:76 +msgid "Search button" +msgstr "" + +#: apps/note/views.py:99 +msgid "Update button" +msgstr "" + +#: apps/note/views.py:136 templates/base.html:94 +msgid "Consumptions" msgstr "" #: apps/permission/models.py:83 #, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" + +#: apps/permission/models.py:85 +#, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:96 +#: apps/permission/models.py:98 msgid "rank" msgstr "" -#: apps/permission/models.py:109 +#: apps/permission/models.py:111 msgid "permission mask" msgstr "" -#: apps/permission/models.py:110 +#: apps/permission/models.py:112 msgid "permission masks" msgstr "" -#: apps/permission/models.py:150 +#: apps/permission/models.py:152 msgid "query" msgstr "" -#: apps/permission/models.py:163 +#: apps/permission/models.py:165 msgid "mask" msgstr "" -#: apps/permission/models.py:169 +#: apps/permission/models.py:171 msgid "field" msgstr "" -#: apps/permission/models.py:174 +#: apps/permission/models.py:176 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." msgstr "" -#: apps/permission/models.py:175 templates/permission/all_rights.html:26 +#: apps/permission/models.py:177 templates/permission/all_rights.html:26 msgid "permanent" msgstr "" -#: apps/permission/models.py:186 +#: apps/permission/models.py:188 msgid "permission" msgstr "" -#: apps/permission/models.py:187 apps/permission/models.py:326 +#: apps/permission/models.py:189 apps/permission/models.py:328 msgid "permissions" msgstr "" -#: apps/permission/models.py:192 +#: apps/permission/models.py:194 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/permission/models.py:331 +#: apps/permission/models.py:333 msgid "for club" msgstr "" -#: apps/permission/models.py:341 apps/permission/models.py:342 +#: apps/permission/models.py:343 apps/permission/models.py:344 msgid "role permissions" msgstr "" -#: apps/permission/signals.py:62 +#: apps/permission/signals.py:63 #, python-brace-format msgid "" "You don't have the permission to change the field {field} on this instance " "of model {app_label}.{model_name}." msgstr "" -#: apps/permission/signals.py:72 +#: apps/permission/signals.py:73 #, python-brace-format msgid "" "You don't have the permission to add this instance of model {app_label}." "{model_name}." msgstr "" -#: apps/permission/signals.py:99 +#: apps/permission/signals.py:101 #, python-brace-format msgid "" "You don't have the permission to delete this instance of model {app_label}." "{model_name}." msgstr "" -#: apps/permission/views.py:48 +#: apps/permission/views.py:44 templates/base.html:135 +msgid "Rights" +msgstr "" + +#: apps/permission/views.py:49 msgid "All rights" msgstr "" @@ -946,35 +1018,59 @@ msgstr "" msgid "Join Kfet Club" msgstr "" -#: apps/registration/views.py:79 +#: apps/registration/views.py:38 +msgid "Register new user" +msgstr "" + +#: apps/registration/views.py:80 msgid "Email validation" msgstr "" -#: apps/registration/views.py:125 +#: apps/registration/views.py:82 +msgid "Validate a registration" +msgstr "" + +#: apps/registration/views.py:127 msgid "Email validation unsuccessful" msgstr "" -#: apps/registration/views.py:136 +#: apps/registration/views.py:138 msgid "Email validation email sent" msgstr "" -#: apps/registration/views.py:189 +#: apps/registration/views.py:146 +msgid "Resend email validation link" +msgstr "" + +#: apps/registration/views.py:164 +msgid "Pre-registered users list" +msgstr "" + +#: apps/registration/views.py:193 msgid "Unregistered users" msgstr "" -#: apps/registration/views.py:256 +#: apps/registration/views.py:206 +msgid "Registration detail" +msgstr "" + +#: apps/registration/views.py:261 msgid "You must join the BDE." msgstr "" -#: apps/registration/views.py:278 +#: apps/registration/views.py:283 msgid "You must join BDE club before joining Kfet club." msgstr "" -#: apps/registration/views.py:283 +#: apps/registration/views.py:288 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" +#: apps/registration/views.py:363 +msgid "Invalidate pre-registration" +msgstr "" + #: apps/treasury/apps.py:12 templates/base.html:126 msgid "Treasury" msgstr "" @@ -1172,6 +1268,43 @@ msgstr "" msgid "No" msgstr "" +#: apps/treasury/views.py:39 +msgid "Create new invoice" +msgstr "" + +#: apps/treasury/views.py:82 templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "" + +#: apps/treasury/views.py:91 +msgid "Update an invoice" +msgstr "" + +#: apps/treasury/views.py:205 +msgid "Create a new remittance" +msgstr "" + +#: apps/treasury/views.py:226 templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "" + +#: apps/treasury/views.py:276 +msgid "Update a remittance" +msgstr "" + +#: apps/treasury/views.py:301 +msgid "Attach a transaction to a remittance" +msgstr "" + +#: apps/treasury/views.py:345 +msgid "List of credits from the Société générale" +msgstr "" + +#: apps/treasury/views.py:384 +msgid "Manage credits from the Société générale" +msgstr "" + #: apps/wei/apps.py:10 apps/wei/models.py:48 apps/wei/models.py:49 #: apps/wei/models.py:60 apps/wei/models.py:166 templates/base.html:131 msgid "WEI" @@ -1389,45 +1522,113 @@ msgstr "" msgid "members" msgstr "" -#: apps/wei/views.py:203 +#: apps/wei/views.py:55 +msgid "Search WEI" +msgstr "" + +#: apps/wei/views.py:64 templates/wei/weiclub_list.html:9 +msgid "Create WEI" +msgstr "" + +#: apps/wei/views.py:84 +msgid "WEI Detail" +msgstr "" + +#: apps/wei/views.py:179 +msgid "View members of the WEI" +msgstr "" + +#: apps/wei/views.py:207 msgid "Find WEI Membership" msgstr "" -#: apps/wei/views.py:238 +#: apps/wei/views.py:217 +msgid "View registrations to the WEI" +msgstr "" + +#: apps/wei/views.py:243 msgid "Find WEI Registration" msgstr "" -#: apps/wei/views.py:447 templates/wei/weiclub_info.html:62 +#: apps/wei/views.py:254 +msgid "Update the WEI" +msgstr "" + +#: apps/wei/views.py:275 +msgid "Create new bus" +msgstr "" + +#: apps/wei/views.py:306 +msgid "Update bus" +msgstr "" + +#: apps/wei/views.py:336 +msgid "Manage bus" +msgstr "" + +#: apps/wei/views.py:363 +msgid "Create new team" +msgstr "" + +#: apps/wei/views.py:395 +msgid "Update team" +msgstr "" + +#: apps/wei/views.py:426 +msgid "Manage WEI team" +msgstr "" + +#: apps/wei/views.py:448 +msgid "Register first year student to the WEI" +msgstr "" + +#: apps/wei/views.py:460 templates/wei/weiclub_info.html:62 msgid "Register 1A" msgstr "" -#: apps/wei/views.py:468 apps/wei/views.py:537 +#: apps/wei/views.py:481 apps/wei/views.py:551 msgid "This user is already registered to this WEI." msgstr "" -#: apps/wei/views.py:473 +#: apps/wei/views.py:486 msgid "" "This user can't be in her/his first year since he/she has already participed " "to a WEI." msgstr "" -#: apps/wei/views.py:501 templates/wei/weiclub_info.html:65 +#: apps/wei/views.py:503 +msgid "Register old student to the WEI" +msgstr "" + +#: apps/wei/views.py:515 templates/wei/weiclub_info.html:65 msgid "Register 2A+" msgstr "" -#: apps/wei/views.py:519 apps/wei/views.py:606 +#: apps/wei/views.py:533 apps/wei/views.py:621 msgid "You already opened an account in the Société générale." msgstr "" -#: apps/wei/views.py:666 +#: apps/wei/views.py:581 +msgid "Update WEI Registration" +msgstr "" + +#: apps/wei/views.py:671 +msgid "Delete WEI registration" +msgstr "" + +#: apps/wei/views.py:682 msgid "You don't have the right to delete this WEI registration." msgstr "" -#: apps/wei/views.py:765 +#: apps/wei/views.py:701 +msgid "Validate WEI registration" +msgstr "" + +#: apps/wei/views.py:782 msgid "This user didn't give her/his caution check." msgstr "" -#: apps/wei/views.py:839 apps/wei/views.py:859 apps/wei/views.py:869 +#: apps/wei/views.py:819 apps/wei/views.py:872 apps/wei/views.py:882 #: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 #: templates/wei/survey_end.html:12 msgid "Survey WEI" @@ -1562,10 +1763,6 @@ msgstr "" msgid "Registrations" msgstr "" -#: templates/base.html:135 -msgid "Rights" -msgstr "" - #: templates/base.html:139 msgid "Administration" msgstr "" @@ -1623,15 +1820,11 @@ msgstr "" msgid "View Profile" msgstr "" -#: templates/member/club_list.html:8 -msgid "search clubs" -msgstr "" - -#: templates/member/club_list.html:12 +#: templates/member/club_list.html:9 msgid "Create club" msgstr "" -#: templates/member/club_list.html:19 +#: templates/member/club_list.html:16 msgid "Club listing" msgstr "" @@ -1675,10 +1868,6 @@ msgstr "" msgid "Change password" msgstr "" -#: templates/member/profile_info.html:43 -msgid "Manage auth token" -msgstr "" - #: templates/member/profile_tables.html:7 #: templates/registration/future_profile_detail.html:28 #: templates/wei/weimembership_form.html:30 @@ -1776,27 +1965,23 @@ msgstr "" msgid "Current price" msgstr "" -#: templates/note/transactiontemplate_list.html:9 -msgid "Search button" -msgstr "" - -#: templates/note/transactiontemplate_list.html:11 +#: templates/note/transactiontemplate_list.html:8 msgid "Name of the button..." msgstr "" -#: templates/note/transactiontemplate_list.html:21 +#: templates/note/transactiontemplate_list.html:10 msgid "New button" msgstr "" -#: templates/note/transactiontemplate_list.html:28 +#: templates/note/transactiontemplate_list.html:17 msgid "buttons listing " msgstr "" -#: templates/note/transactiontemplate_list.html:86 +#: templates/note/transactiontemplate_list.html:55 msgid "button successfully deleted " msgstr "" -#: templates/note/transactiontemplate_list.html:90 +#: templates/note/transactiontemplate_list.html:59 msgid "Unable to delete button " msgstr "" @@ -1981,10 +2166,6 @@ msgid "" "by following the link you received." msgstr "" -#: templates/treasury/invoice_form.html:6 -msgid "Invoices list" -msgstr "" - #: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "" @@ -2007,11 +2188,6 @@ msgstr "" msgid "Remittance #" msgstr "" -#: templates/treasury/remittance_form.html:9 -#: templates/treasury/specialtransactionproxy_form.html:7 -msgid "Remittances list" -msgstr "" - #: templates/treasury/remittance_form.html:12 msgid "Count" msgstr "" @@ -2161,15 +2337,7 @@ msgstr "" msgid "View WEI" msgstr "" -#: templates/wei/weiclub_list.html:8 -msgid "search WEI" -msgstr "" - -#: templates/wei/weiclub_list.html:12 -msgid "Create WEI" -msgstr "" - -#: templates/wei/weiclub_list.html:19 +#: templates/wei/weiclub_list.html:16 msgid "WEI listing" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 2f499334..35ecff90 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-29 22:54+0200\n" +"POT-Creation-Date: 2020-07-30 17:22+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,7 +47,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/note/models/notes.py:188 #: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 -#: apps/note/models/transactions.py:261 apps/permission/models.py:321 +#: apps/note/models/transactions.py:261 apps/permission/models.py:323 #: apps/wei/models.py:65 apps/wei/models.py:117 #: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -72,14 +72,14 @@ msgid "activity types" msgstr "types d'activité" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:102 apps/permission/models.py:181 +#: apps/permission/models.py:104 apps/permission/models.py:183 #: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "description" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:156 +#: apps/note/models/transactions.py:66 apps/permission/models.py:158 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" @@ -187,12 +187,12 @@ msgstr "supprimer" msgid "Type" msgstr "Type" -#: apps/activity/tables.py:77 apps/member/forms.py:105 +#: apps/activity/tables.py:77 apps/member/forms.py:104 #: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "Nom de famille" -#: apps/activity/tables.py:79 apps/member/forms.py:110 +#: apps/activity/tables.py:79 apps/member/forms.py:109 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" @@ -206,11 +206,27 @@ msgstr "Note" msgid "Balance" msgstr "Solde du compte" -#: apps/activity/views.py:47 templates/base.html:121 +#: apps/activity/views.py:26 +msgid "Create new activity" +msgstr "Créer une nouvelle activité" + +#: apps/activity/views.py:41 templates/base.html:121 msgid "Activities" msgstr "Activités" -#: apps/activity/views.py:163 +#: apps/activity/views.py:61 +msgid "Activity detail" +msgstr "Détails de l'activité" + +#: apps/activity/views.py:78 +msgid "Update activity" +msgstr "Modifier l'activité" + +#: apps/activity/views.py:92 +msgid "Invite guest to the activity \"{}\"" +msgstr "Invitation pour l'activité « {} »" + +#: apps/activity/views.py:171 msgid "Entry for activity \"{}\"" msgstr "Entrées pour l'activité « {} »" @@ -226,7 +242,7 @@ msgstr "Logs" msgid "IP Address" msgstr "Adresse IP" -#: apps/logs/models.py:35 apps/permission/models.py:126 +#: apps/logs/models.py:35 apps/permission/models.py:128 msgid "model" msgstr "Modèle" @@ -246,12 +262,12 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 apps/note/tables.py:145 +#: apps/logs/models.py:61 apps/note/tables.py:143 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "Modifier" -#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:150 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:148 #: apps/wei/tables.py:65 msgid "delete" msgstr "Supprimer" @@ -280,35 +296,35 @@ msgstr "journaux de modifications" msgid "member" msgstr "adhérent" -#: apps/member/forms.py:59 apps/member/views.py:78 +#: apps/member/forms.py:58 apps/member/views.py:81 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/forms.py:84 apps/registration/forms.py:44 +#: apps/member/forms.py:83 apps/registration/forms.py:44 msgid "Inscription paid by Société Générale" msgstr "Inscription payée par la Société générale" -#: apps/member/forms.py:86 apps/registration/forms.py:46 +#: apps/member/forms.py:85 apps/registration/forms.py:46 msgid "Check this case is the Société Générale paid the inscription." msgstr "Cochez cette case si la Société Générale a payé l'inscription." -#: apps/member/forms.py:91 apps/registration/forms.py:51 +#: apps/member/forms.py:90 apps/registration/forms.py:51 msgid "Credit type" msgstr "Type de rechargement" -#: apps/member/forms.py:92 apps/registration/forms.py:52 +#: apps/member/forms.py:91 apps/registration/forms.py:52 msgid "No credit" msgstr "Pas de rechargement" -#: apps/member/forms.py:94 +#: apps/member/forms.py:93 msgid "You can credit the note of the user." msgstr "Vous pouvez créditer la note de l'utisateur avant l'adhésion." -#: apps/member/forms.py:98 apps/registration/forms.py:57 +#: apps/member/forms.py:97 apps/registration/forms.py:57 msgid "Credit amount" msgstr "Montant à créditer" -#: apps/member/forms.py:115 apps/registration/forms.py:74 +#: apps/member/forms.py:114 apps/registration/forms.py:74 #: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "Banque" @@ -514,7 +530,7 @@ msgstr "l'adhésion finit le" msgid "fee" msgstr "cotisation" -#: apps/member/models.py:303 apps/member/views.py:528 apps/wei/views.py:770 +#: apps/member/models.py:303 apps/member/views.py:535 apps/wei/views.py:787 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" @@ -523,7 +539,7 @@ msgstr "L'utilisateur n'est pas membre du club parent" msgid "The role {role} does not apply to the club {club}." msgstr "Le rôle {role} ne s'applique pas au club {club}." -#: apps/member/models.py:321 apps/member/views.py:537 +#: apps/member/models.py:321 apps/member/views.py:544 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" @@ -544,21 +560,57 @@ msgstr "adhésions" msgid "Renew" msgstr "Renouveler" -#: apps/member/views.py:65 apps/registration/forms.py:23 -msgid "This address must be valid." -msgstr "Cette adresse doit être valide." - -#: apps/member/views.py:68 templates/member/profile_info.html:47 +#: apps/member/views.py:56 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:130 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:184 +#: apps/member/views.py:69 apps/registration/forms.py:23 +msgid "This address must be valid." +msgstr "Cette adresse doit être valide." + +#: apps/member/views.py:126 +msgid "Profile detail" +msgstr "Détails de l'utilisateur" + +#: apps/member/views.py:160 msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:523 apps/wei/views.py:761 +#: apps/member/views.py:194 apps/member/views.py:376 +msgid "Note aliases" +msgstr "Alias de la note" + +#: apps/member/views.py:208 +msgid "Update note picture" +msgstr "Modifier la photo de la note" + +#: apps/member/views.py:266 templates/member/profile_info.html:43 +msgid "Manage auth token" +msgstr "Gérer les jetons d'authentification" + +#: apps/member/views.py:294 +msgid "Create new club" +msgstr "Créer un nouveau club" + +#: apps/member/views.py:306 +msgid "Search club" +msgstr "Chercher un club" + +#: apps/member/views.py:331 +msgid "Club detail" +msgstr "Détails du club" + +#: apps/member/views.py:393 +msgid "Update club" +msgstr "Modifier le club" + +#: apps/member/views.py:427 +msgid "Add new member to the club" +msgstr "Ajouter un nouveau membre au club" + +#: apps/member/views.py:530 apps/wei/views.py:778 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -566,25 +618,29 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:541 +#: apps/member/views.py:548 msgid "The membership must start after {:%m-%d-%Y}." msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." -#: apps/member/views.py:546 +#: apps/member/views.py:553 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." -#: apps/member/views.py:563 apps/member/views.py:565 apps/member/views.py:567 -#: apps/registration/views.py:290 apps/registration/views.py:292 -#: apps/registration/views.py:294 +#: apps/member/views.py:570 apps/member/views.py:572 apps/member/views.py:574 +#: apps/registration/views.py:295 apps/registration/views.py:297 +#: apps/registration/views.py:299 msgid "This field is required." msgstr "Ce champ est requis." -#: apps/note/admin.py:122 apps/note/models/transactions.py:106 +#: apps/member/views.py:634 +msgid "Manage roles of an user in the club" +msgstr "Gérer les rôles d'un utilisateur dans le club" + +#: apps/note/admin.py:121 apps/note/models/transactions.py:106 msgid "source" msgstr "source" -#: apps/note/admin.py:130 apps/note/admin.py:172 +#: apps/note/admin.py:129 apps/note/admin.py:171 #: apps/note/models/transactions.py:55 apps/note/models/transactions.py:119 msgid "destination" msgstr "destination" @@ -826,62 +882,74 @@ msgstr "Cliquez pour valider" msgid "No reason specified" msgstr "Pas de motif spécifié" -#: apps/note/tables.py:122 apps/note/tables.py:152 apps/wei/tables.py:66 +#: apps/note/tables.py:122 apps/note/tables.py:150 apps/wei/tables.py:66 #: templates/treasury/sogecredit_detail.html:59 #: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "Supprimer" -#: apps/note/tables.py:147 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: apps/note/tables.py:145 apps/wei/tables.py:42 apps/wei/tables.py:43 #: templates/member/club_info.html:67 templates/note/conso_form.html:128 #: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 #: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 msgid "Edit" msgstr "Éditer" -#: apps/note/views.py:41 +#: apps/note/views.py:33 msgid "Transfer money" msgstr "Transférer de l'argent" -#: apps/note/views.py:140 templates/base.html:94 +#: apps/note/views.py:67 +msgid "Create new button" +msgstr "Créer un nouveau bouton" + +#: apps/note/views.py:76 +msgid "Search button" +msgstr "Chercher un bouton" + +#: apps/note/views.py:99 +msgid "Update button" +msgstr "Modifier le bouton" + +#: apps/note/views.py:136 templates/base.html:94 msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:81 +#: apps/permission/models.py:83 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "Can {type} {model}.{field} in {query}" -#: apps/permission/models.py:83 +#: apps/permission/models.py:85 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "Can {type} {model} in {query}" -#: apps/permission/models.py:96 +#: apps/permission/models.py:98 msgid "rank" msgstr "Rang" -#: apps/permission/models.py:109 +#: apps/permission/models.py:111 msgid "permission mask" msgstr "masque de permissions" -#: apps/permission/models.py:110 +#: apps/permission/models.py:112 msgid "permission masks" msgstr "masques de permissions" -#: apps/permission/models.py:150 +#: apps/permission/models.py:152 msgid "query" msgstr "requête" -#: apps/permission/models.py:163 +#: apps/permission/models.py:165 msgid "mask" msgstr "masque" -#: apps/permission/models.py:169 +#: apps/permission/models.py:171 msgid "field" msgstr "champ" -#: apps/permission/models.py:174 +#: apps/permission/models.py:176 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." @@ -889,33 +957,33 @@ msgstr "" "Indique si la permission doit être attribuée même si l'adhésion de " "l'utilisateur est expirée." -#: apps/permission/models.py:175 templates/permission/all_rights.html:26 +#: apps/permission/models.py:177 templates/permission/all_rights.html:26 msgid "permanent" msgstr "permanent" -#: apps/permission/models.py:186 +#: apps/permission/models.py:188 msgid "permission" msgstr "permission" -#: apps/permission/models.py:187 apps/permission/models.py:326 +#: apps/permission/models.py:189 apps/permission/models.py:328 msgid "permissions" msgstr "permissions" -#: apps/permission/models.py:192 +#: apps/permission/models.py:194 msgid "Specifying field applies only to view and change permission types." msgstr "" "Spécifie le champ concerné, ne fonctionne que pour les permissions view et " "change." -#: apps/permission/models.py:331 +#: apps/permission/models.py:333 msgid "for club" msgstr "s'applique au club" -#: apps/permission/models.py:341 apps/permission/models.py:342 +#: apps/permission/models.py:343 apps/permission/models.py:344 msgid "role permissions" msgstr "Permissions par rôles" -#: apps/permission/signals.py:62 +#: apps/permission/signals.py:63 #, python-brace-format msgid "" "You don't have the permission to change the field {field} on this instance " @@ -924,7 +992,7 @@ msgstr "" "Vous n'avez pas la permission de modifier le champ {field} sur l'instance du " "modèle {app_label}.{model_name}." -#: apps/permission/signals.py:72 +#: apps/permission/signals.py:73 #, python-brace-format msgid "" "You don't have the permission to add this instance of model {app_label}." @@ -933,7 +1001,7 @@ msgstr "" "Vous n'avez pas la permission d'ajouter cette instance du modèle {app_label}." "{model_name}." -#: apps/permission/signals.py:99 +#: apps/permission/signals.py:101 #, python-brace-format msgid "" "You don't have the permission to delete this instance of model {app_label}." @@ -942,7 +1010,11 @@ msgstr "" "Vous n'avez pas la permission de supprimer cette instance du modèle " "{app_label}.{model_name}." -#: apps/permission/views.py:48 +#: apps/permission/views.py:44 templates/base.html:135 +msgid "Rights" +msgstr "Droits" + +#: apps/permission/views.py:49 msgid "All rights" msgstr "Tous les droits" @@ -971,37 +1043,61 @@ msgstr "Adhérer au club BDE" msgid "Join Kfet Club" msgstr "Adhérer au club Kfet" -#: apps/registration/views.py:79 +#: apps/registration/views.py:38 +msgid "Register new user" +msgstr "Enregistrer un nouvel utilisateur" + +#: apps/registration/views.py:80 msgid "Email validation" msgstr "Validation de l'adresse mail" -#: apps/registration/views.py:125 +#: apps/registration/views.py:82 +msgid "Validate a registration" +msgstr "Valider l'inscription" + +#: apps/registration/views.py:127 msgid "Email validation unsuccessful" msgstr " La validation de l'adresse mail a échoué" -#: apps/registration/views.py:136 +#: apps/registration/views.py:138 msgid "Email validation email sent" msgstr "L'email de vérification de l'adresse email a bien été envoyé." -#: apps/registration/views.py:189 +#: apps/registration/views.py:146 +msgid "Resend email validation link" +msgstr "Renvoyer le lien de validation" + +#: apps/registration/views.py:164 +msgid "Pre-registered users list" +msgstr "Liste des utilisateurs en attente d'inscription" + +#: apps/registration/views.py:193 msgid "Unregistered users" msgstr "Utilisateurs en attente d'inscription" -#: apps/registration/views.py:256 +#: apps/registration/views.py:206 +msgid "Registration detail" +msgstr "Détails de l'inscription" + +#: apps/registration/views.py:261 msgid "You must join the BDE." msgstr "Vous devez adhérer au BDE." -#: apps/registration/views.py:278 +#: apps/registration/views.py:283 msgid "You must join BDE club before joining Kfet club." msgstr "Vous devez adhérer au club BDE avant d'adhérer au club Kfet." -#: apps/registration/views.py:283 +#: apps/registration/views.py:288 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" "Le montant crédité est trop faible pour adhérer, il doit être au minimum de " "{}" +#: apps/registration/views.py:363 +msgid "Invalidate pre-registration" +msgstr "Invalider l'inscription" + #: apps/treasury/apps.py:12 templates/base.html:126 msgid "Treasury" msgstr "Trésorerie" @@ -1201,6 +1297,43 @@ msgstr "Oui" msgid "No" msgstr "Non" +#: apps/treasury/views.py:39 +msgid "Create new invoice" +msgstr "Créer une nouvelle facture" + +#: apps/treasury/views.py:82 templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "Liste des factures" + +#: apps/treasury/views.py:91 +msgid "Update an invoice" +msgstr "Modifier la facture" + +#: apps/treasury/views.py:205 +msgid "Create a new remittance" +msgstr "Créer une nouvelle remise" + +#: apps/treasury/views.py:226 templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "Liste des remises" + +#: apps/treasury/views.py:276 +msgid "Update a remittance" +msgstr "Modifier la remise" + +#: apps/treasury/views.py:301 +msgid "Attach a transaction to a remittance" +msgstr "Joindre une transaction à une remise" + +#: apps/treasury/views.py:345 +msgid "List of credits from the Société générale" +msgstr "Liste des crédits de la Société générale" + +#: apps/treasury/views.py:384 +msgid "Manage credits from the Société générale" +msgstr "Gérer les crédits de la Société générale" + #: apps/wei/apps.py:10 apps/wei/models.py:48 apps/wei/models.py:49 #: apps/wei/models.py:60 apps/wei/models.py:166 templates/base.html:131 msgid "WEI" @@ -1433,23 +1566,75 @@ msgstr "Nombre de membres" msgid "members" msgstr "adhérents" -#: apps/wei/views.py:203 +#: apps/wei/views.py:55 +msgid "Search WEI" +msgstr "Chercher un WEI" + +#: apps/wei/views.py:64 templates/wei/weiclub_list.html:9 +msgid "Create WEI" +msgstr "Créer un WEI" + +#: apps/wei/views.py:84 +msgid "WEI Detail" +msgstr "Détails du WEI" + +#: apps/wei/views.py:179 +msgid "View members of the WEI" +msgstr "Voir les membres du WEI" + +#: apps/wei/views.py:207 msgid "Find WEI Membership" msgstr "Trouver une adhésion au WEI" -#: apps/wei/views.py:238 +#: apps/wei/views.py:217 +msgid "View registrations to the WEI" +msgstr "Voir les inscriptions au WEI" + +#: apps/wei/views.py:243 msgid "Find WEI Registration" msgstr "Trouver une inscription au WEI" -#: apps/wei/views.py:447 templates/wei/weiclub_info.html:62 +#: apps/wei/views.py:254 +msgid "Update the WEI" +msgstr "Modifier le WEI" + +#: apps/wei/views.py:275 +msgid "Create new bus" +msgstr "Ajouter un nouveau bus" + +#: apps/wei/views.py:306 +msgid "Update bus" +msgstr "Modifier le bus" + +#: apps/wei/views.py:336 +msgid "Manage bus" +msgstr "Gérer le bus" + +#: apps/wei/views.py:363 +msgid "Create new team" +msgstr "Créer une nouvelle équipe" + +#: apps/wei/views.py:395 +msgid "Update team" +msgstr "Modifier l'équipe" + +#: apps/wei/views.py:426 +msgid "Manage WEI team" +msgstr "Gérer l'équipe WEI" + +#: apps/wei/views.py:448 +msgid "Register first year student to the WEI" +msgstr "Inscrire un 1A au WEI" + +#: apps/wei/views.py:460 templates/wei/weiclub_info.html:62 msgid "Register 1A" msgstr "Inscrire un 1A" -#: apps/wei/views.py:468 apps/wei/views.py:537 +#: apps/wei/views.py:481 apps/wei/views.py:551 msgid "This user is already registered to this WEI." msgstr "Cette personne est déjà inscrite au WEI." -#: apps/wei/views.py:473 +#: apps/wei/views.py:486 msgid "" "This user can't be in her/his first year since he/she has already participed " "to a WEI." @@ -1457,23 +1642,39 @@ msgstr "" "Cet utilisateur ne peut pas être en première année puisqu'iel a déjà " "participé à un WEI." -#: apps/wei/views.py:501 templates/wei/weiclub_info.html:65 +#: apps/wei/views.py:503 +msgid "Register old student to the WEI" +msgstr "Inscrire un 2A+ au WEI" + +#: apps/wei/views.py:515 templates/wei/weiclub_info.html:65 msgid "Register 2A+" msgstr "Inscrire un 2A+" -#: apps/wei/views.py:519 apps/wei/views.py:606 +#: apps/wei/views.py:533 apps/wei/views.py:621 msgid "You already opened an account in the Société générale." msgstr "Vous avez déjà ouvert un compte auprès de la société générale." -#: apps/wei/views.py:666 +#: apps/wei/views.py:581 +msgid "Update WEI Registration" +msgstr "Modifier l'inscription WEI" + +#: apps/wei/views.py:671 +msgid "Delete WEI registration" +msgstr "Supprimer l'inscription WEI" + +#: apps/wei/views.py:682 msgid "You don't have the right to delete this WEI registration." msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI." -#: apps/wei/views.py:765 +#: apps/wei/views.py:701 +msgid "Validate WEI registration" +msgstr "Valider l'inscription WEI" + +#: apps/wei/views.py:782 msgid "This user didn't give her/his caution check." msgstr "Cet utilisateur n'a pas donné son chèque de caution." -#: apps/wei/views.py:839 apps/wei/views.py:859 apps/wei/views.py:869 +#: apps/wei/views.py:819 apps/wei/views.py:872 apps/wei/views.py:882 #: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 #: templates/wei/survey_end.html:12 msgid "Survey WEI" @@ -1617,15 +1818,9 @@ msgstr "Clubs" msgid "Registrations" msgstr "Inscriptions" -#: templates/base.html:135 -msgid "Rights" -msgstr "Droits" - #: templates/base.html:139 -#, fuzzy -#| msgid "registration" msgid "Administration" -msgstr "inscription" +msgstr "Administration" #: templates/base.html:178 msgid "" @@ -1685,15 +1880,11 @@ msgstr "Ajouter un membre" msgid "View Profile" msgstr "Voir le profil" -#: templates/member/club_list.html:8 -msgid "search clubs" -msgstr "Chercher un club" - -#: templates/member/club_list.html:12 +#: templates/member/club_list.html:9 msgid "Create club" msgstr "Créer un club" -#: templates/member/club_list.html:19 +#: templates/member/club_list.html:16 msgid "Club listing" msgstr "Liste des clubs" @@ -1737,10 +1928,6 @@ msgstr "mot de passe" msgid "Change password" msgstr "Changer le mot de passe" -#: templates/member/profile_info.html:43 -msgid "Manage auth token" -msgstr "Gérer les jetons d'authentification" - #: templates/member/profile_tables.html:7 #: templates/registration/future_profile_detail.html:28 #: templates/wei/weimembership_form.html:30 @@ -1838,27 +2025,23 @@ msgstr "Obsolète depuis" msgid "Current price" msgstr "Prix actuel" -#: templates/note/transactiontemplate_list.html:9 -msgid "Search button" -msgstr "Chercher un bouton" - -#: templates/note/transactiontemplate_list.html:11 +#: templates/note/transactiontemplate_list.html:8 msgid "Name of the button..." msgstr "Nom du bouton ..." -#: templates/note/transactiontemplate_list.html:21 +#: templates/note/transactiontemplate_list.html:10 msgid "New button" msgstr "Nouveau bouton" -#: templates/note/transactiontemplate_list.html:28 +#: templates/note/transactiontemplate_list.html:17 msgid "buttons listing " msgstr "Liste des boutons" -#: templates/note/transactiontemplate_list.html:86 +#: templates/note/transactiontemplate_list.html:55 msgid "button successfully deleted " msgstr "Le bouton a bien été supprimé" -#: templates/note/transactiontemplate_list.html:90 +#: templates/note/transactiontemplate_list.html:59 msgid "Unable to delete button " msgstr "Impossible de supprimer le bouton " @@ -2073,10 +2256,6 @@ msgstr "" "Vous devez également valider votre adresse email en suivant le lien que vous " "avez reçu." -#: templates/treasury/invoice_form.html:6 -msgid "Invoices list" -msgstr "Liste des factures" - #: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "Ajouter produit" @@ -2099,11 +2278,6 @@ msgstr "Nouvelle facture" msgid "Remittance #" msgstr "Remise n°" -#: templates/treasury/remittance_form.html:9 -#: templates/treasury/specialtransactionproxy_form.html:7 -msgid "Remittances list" -msgstr "Liste des remises" - #: templates/treasury/remittance_form.html:12 msgid "Count" msgstr "Nombre" @@ -2267,15 +2441,7 @@ msgstr "Ajouter un bus" msgid "View WEI" msgstr "Voir le WEI" -#: templates/wei/weiclub_list.html:8 -msgid "search WEI" -msgstr "Chercher un WEI" - -#: templates/wei/weiclub_list.html:12 -msgid "Create WEI" -msgstr "Créer un WEI" - -#: templates/wei/weiclub_list.html:19 +#: templates/wei/weiclub_list.html:16 msgid "WEI listing" msgstr "Liste des WEI" @@ -2289,7 +2455,7 @@ msgstr "M'inscrire au WEI ! – 2A+" #: templates/wei/weiclub_tables.html:67 msgid "Update my registration" -msgstr "Mettre à jour mon inscription" +msgstr "Modifier mon inscription" #: templates/wei/weiclub_tables.html:92 msgid "Members of the WEI" @@ -2342,7 +2508,7 @@ msgstr "rôles préférés" #: templates/wei/weimembership_form.html:128 #: templates/wei/weiregistration_confirm_delete.html:31 msgid "Update registration" -msgstr "Mettre à jour l'inscription" +msgstr "Modifier l'inscription" #: templates/wei/weimembership_form.html:144 msgid "The registration is already validated and can't be unvalidated." diff --git a/templates/member/club_list.html b/templates/member/club_list.html index a69de324..346cc623 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -4,9 +4,6 @@ {% block content %}
    -

    - {% trans "search clubs" %} -


    {% trans "Create club" %} diff --git a/templates/note/transactiontemplate_list.html b/templates/note/transactiontemplate_list.html index 1d2342b9..793b2278 100644 --- a/templates/note/transactiontemplate_list.html +++ b/templates/note/transactiontemplate_list.html @@ -5,9 +5,6 @@ {% block content %}
    -

    - {% trans "Search button" %} -


    {% trans "New button" %} diff --git a/templates/wei/weiclub_list.html b/templates/wei/weiclub_list.html index 0ed8e0ac..5390ade6 100644 --- a/templates/wei/weiclub_list.html +++ b/templates/wei/weiclub_list.html @@ -4,9 +4,6 @@ {% block content %}
    -

    - {% trans "search WEI" %} -


    {% trans "Create WEI" %} From ad2cc2296443e0a892848ee19a6c70ace20858dd Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 17:52:04 +0200 Subject: [PATCH 56/77] Transactions are not invalidable if the user doesn't have the right to --- apps/note/tables.py | 28 +++++++++++++++++++++++----- apps/treasury/views.py | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/note/tables.py b/apps/note/tables.py index cbfd2424..0048a0a5 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -8,6 +8,8 @@ from django.db.models import F from django.utils.html import format_html from django_tables2.utils import A from django.utils.translation import gettext_lazy as _ +from note_kfet.middlewares import get_current_authenticated_user +from permission.backends import PermissionBackend from .models.notes import Alias from .models.transactions import Transaction, TransactionTemplate @@ -52,14 +54,26 @@ class HistoryTable(tables.Table): attrs={ "td": { "id": lambda record: "validate_" + str(record.id), - "class": lambda record: str(record.valid).lower() + ' validate', + "class": lambda record: + str(record.valid).lower() + + (' validate' if PermissionBackend.check_perm(get_current_authenticated_user(), + "note.change_transaction_invalidity_reason", + record) else ''), "data-toggle": "tooltip", - "title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"), - "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')', + "title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate")) + if PermissionBackend.check_perm(get_current_authenticated_user(), + "note.change_transaction_invalidity_reason", record) else None, + "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')' + if PermissionBackend.check_perm(get_current_authenticated_user(), + "note.change_transaction_invalidity_reason", record) else None, "onmouseover": lambda record: '$("#invalidity_reason_' + str(record.id) + '").show();$("#invalidity_reason_' - + str(record.id) + '").focus();', - "onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()', + + str(record.id) + '").focus();' + if PermissionBackend.check_perm(get_current_authenticated_user(), + "note.change_transaction_invalidity_reason", record) else None, + "onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()' + if PermissionBackend.check_perm(get_current_authenticated_user(), + "note.change_transaction_invalidity_reason", record) else None, } } ) @@ -88,6 +102,10 @@ class HistoryTable(tables.Table): When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason """ val = "✔" if value else "✖" + if not PermissionBackend\ + .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record): + return val + val += " Date: Thu, 30 Jul 2020 20:41:19 +0200 Subject: [PATCH 57/77] :bug: Scripts subproject has disappeared --- apps/scripts | 1 + 1 file changed, 1 insertion(+) create mode 160000 apps/scripts diff --git a/apps/scripts b/apps/scripts new file mode 160000 index 00000000..9dcb2572 --- /dev/null +++ b/apps/scripts @@ -0,0 +1 @@ +Subproject commit 9dcb25723e1cf14c2344946a96e9d8f98cc4cf0e From ad19b64b3ac051c3ec1d76620835a2bf5f679e58 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Thu, 30 Jul 2020 20:45:17 +0200 Subject: [PATCH 58/77] :green_heart: Add Python 3.8 to Gitlab CI --- .gitlab-ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 291ed490..152da589 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: python:3.6 +image: python:3.8 stages: - test @@ -17,8 +17,13 @@ py37-django22: stage: test script: tox -e py37-django22 +py38-django22: + image: python:3.8 + stage: test + script: tox -e py38-django22 + linters: - image: python:3.6 + image: python:3.8 stage: quality-assurance script: tox -e linters From cb38ceb2c6b3a52241c79664046f60a9336a5a1a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 09:41:22 +0200 Subject: [PATCH 59/77] New club members have only the role "Club member" by default and no other one --- apps/member/forms.py | 28 +++++++++++++++++++++++++--- apps/member/views.py | 22 ++++++---------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/apps/member/forms.py b/apps/member/forms.py index a31acb80..c4849e2a 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -77,8 +77,6 @@ class ClubForm(forms.ModelForm): class MembershipForm(forms.ModelForm): - roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all()) - soge = forms.BooleanField( label=_("Inscription paid by Société Générale"), required=False, @@ -117,7 +115,7 @@ class MembershipForm(forms.ModelForm): class Meta: model = Membership - fields = ('user', 'roles', 'date_start') + fields = ('user', 'date_start') # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion # et récupère les noms d'utilisateur valides @@ -133,3 +131,27 @@ class MembershipForm(forms.ModelForm): ), 'date_start': DatePickerInput(), } + +class MembershipRolesForm(forms.ModelForm): + user = forms.ModelChoiceField( + queryset=User.objects, + label=_("User"), + disabled=True, + widget=Autocomplete( + User, + attrs={ + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', + }, + ), + ) + + roles = forms.ModelMultipleChoiceField( + queryset=Role.objects.filter(weirole=None).all(), + label=_("Roles"), + ) + + class Meta: + model = Membership + fields = ('user', 'roles') \ No newline at end of file diff --git a/apps/member/views.py b/apps/member/views.py index bc0ed3dd..8e5a0f68 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -27,7 +27,7 @@ from permission.backends import PermissionBackend from permission.models import Role from permission.views import ProtectQuerysetMixin -from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm +from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm from .models import Club, Membership from .tables import ClubTable, UserTable, MembershipTable @@ -435,9 +435,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ .get(pk=self.kwargs["club_pk"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) - & (Q(for_club__isnull=True) | Q(for_club=club))).all() - form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() # If the concerned club is the BDE, then we add the option that Société générale pays the membership. if club.name != "BDE": @@ -456,9 +453,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): user = old_membership.user form.fields['user'].initial = user form.fields['user'].disabled = True - form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) - & (Q(for_club__isnull=True) | Q(for_club=club))).all() - form.fields['roles'].initial = old_membership.roles.all() form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1) form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \ else club.membership_fee_unpaid @@ -588,6 +582,10 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): ret = super().form_valid(form) + member_role = Role.objects.filter(name="Membre de club").all() + form.instance.roles.set(member_role) + form.instance.save() + # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the # Kfet membership. if soge: @@ -629,7 +627,7 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): Manage the roles of a user in a club """ model = Membership - form_class = MembershipForm + form_class = MembershipRolesForm template_name = 'member/add_members.html' extra_context = {"title": _("Manage roles of an user in the club")} @@ -641,14 +639,6 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): def get_form(self, form_class=None): form = super().get_form(form_class) - # We don't create a full membership, we only update one field - form.fields['user'].disabled = True - del form.fields['date_start'] - del form.fields['credit_type'] - del form.fields['credit_amount'] - del form.fields['last_name'] - del form.fields['first_name'] - del form.fields['bank'] club = self.object.club form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) From 72dcc931365f22c2271cc2becca1418aa8d3fd67 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 09:49:43 +0200 Subject: [PATCH 60/77] Club managers can register new members to a club, even if they don't have the right to create a transaction --- apps/member/views.py | 6 +++++- apps/permission/fixtures/initial.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/member/views.py b/apps/member/views.py index 8e5a0f68..51f31445 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -568,7 +568,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): form.add_error('bank', _("This field is required.")) return self.form_invalid(form) - SpecialTransaction.objects.create( + transaction = SpecialTransaction( source=credit_type, destination=user.note, quantity=1, @@ -579,11 +579,14 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): bank=bank, valid=True, ) + transaction._force_save = True + transaction.save() ret = super().form_valid(form) member_role = Role.objects.filter(name="Membre de club").all() form.instance.roles.set(member_role) + form.instance._force_save = True form.instance.save() # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the @@ -607,6 +610,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): date_start=old_membership.get().date_end + timedelta(days=1) if old_membership.exists() else form.instance.date_start, ) + membership._force_save = True membership._soge = True membership.save() membership.refresh_from_db() diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 572e0716..2622ed1c 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -2372,6 +2372,7 @@ 22, 47, 49, + 50, 140 ] } From bd35e4e21e6d90befeaf10a7156e921ab55c40ff Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 13:17:16 +0200 Subject: [PATCH 61/77] Separate club members in a dedicated page (WIP) --- apps/member/urls.py | 1 + apps/member/views.py | 40 +++++++++++++++++++++++++++- locale/de/LC_MESSAGES/django.po | 2 +- locale/fr/LC_MESSAGES/django.po | 4 +-- templates/member/club_info.html | 2 +- templates/member/club_members.html | 42 ++++++++++++++++++++++++++++++ templates/member/club_tables.html | 4 +-- 7 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 templates/member/club_members.html diff --git a/apps/member/urls.py b/apps/member/urls.py index 4be4ceae..359a8846 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -16,6 +16,7 @@ urlpatterns = [ path('club//update/', views.ClubUpdateView.as_view(), name="club_update"), path('club//update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), path('club//aliases/', views.ClubAliasView.as_view(), name="club_alias"), + path('club//members/', views.ClubMembersListView.as_view(), name="club_members"), path('user/', views.UserListView.as_view(), name="user_list"), path('user//', views.UserDetailView.as_view(), name="user_detail"), diff --git a/apps/member/views.py b/apps/member/views.py index 51f31445..1785d1ad 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -13,6 +13,7 @@ from django.contrib.auth.views import LoginView from django.db.models import Q from django.shortcuts import redirect from django.urls import reverse_lazy +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic.edit import FormMixin @@ -349,7 +350,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) membership_table = MembershipTable(data=club_member, prefix="membership-") - membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) + membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1)) context['member_list'] = membership_table # Check if the user has the right to create a membership, to display the button. @@ -652,3 +653,40 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): def get_success_url(self): return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) + + +class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + model = Membership + table_class = MembershipTable + template_name = "member/club_members.html" + extra_context = {"title": _("Members of the club")} + + def get_queryset(self, **kwargs): + qs = super().get_queryset().filter(club_id=self.kwargs["pk"]) + + if 'search' in self.request.GET: + pattern = self.request.GET['search'] + qs = qs.filter( + Q(user__first_name__iregex='^' + pattern) | + Q(user__last_name__iregex='^' + pattern) | + Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern)) + ) + + if 'only_active' in self.request.GET: + only_active = self.request.GET["only_active"] != '0' + else: + only_active = True + + if only_active: + qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today()) + + qs = qs.order_by('-date_start', 'user__username') + + return qs.distinct() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = Club.objects.filter( + PermissionBackend.filter_queryset(self.request.user, Club, "view") + ).get(pk=self.kwargs["pk"]) + return context \ No newline at end of file diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index e4526352..c9ac11ba 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -1829,7 +1829,7 @@ msgid "Club listing" msgstr "" #: templates/member/club_tables.html:7 -msgid "Member of the Club" +msgid "Club members" msgstr "" #: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 35ecff90..c561abba 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -1889,8 +1889,8 @@ msgid "Club listing" msgstr "Liste des clubs" #: templates/member/club_tables.html:7 -msgid "Member of the Club" -msgstr "Membre du club" +msgid "Club members" +msgstr "Membres du club" #: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 #: templates/wei/weiclub_tables.html:105 diff --git a/templates/member/club_info.html b/templates/member/club_info.html index 18cfe631..e7974f77 100644 --- a/templates/member/club_info.html +++ b/templates/member/club_info.html @@ -11,7 +11,7 @@
    {% trans 'name'|capfirst %}
    -
    {{ club.name}}
    +
    {{ club.name }}
    {% if club.parent_club %}
    {% trans 'Club Parent'|capfirst %}
    diff --git a/templates/member/club_members.html b/templates/member/club_members.html new file mode 100644 index 00000000..fca6ec1c --- /dev/null +++ b/templates/member/club_members.html @@ -0,0 +1,42 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block profile_info %} +{% include "member/club_info.html" %} +{% endblock %} + +{% block profile_content %} + +
    + +
    + {% if table.data %} + {% render_table table %} + {% else %} +
    + {% trans "There is no membership found with this pattern." %} +
    + {% endif %} +
    +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/member/club_tables.html b/templates/member/club_tables.html index e937fbe9..92d66425 100644 --- a/templates/member/club_tables.html +++ b/templates/member/club_tables.html @@ -3,8 +3,8 @@ {% if member_list.data %}
    {% render_table member_list %} From fd705adb05b422b54ae2af177bdfda619c851136 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 17:01:52 +0200 Subject: [PATCH 62/77] Filter members --- apps/member/tables.py | 28 +++++++ apps/member/views.py | 28 +++++-- locale/de/LC_MESSAGES/django.po | 113 ++++++++++++++++++----------- locale/fr/LC_MESSAGES/django.po | 113 ++++++++++++++++++----------- templates/member/club_members.html | 55 ++++++++++---- templates/member/club_tables.html | 13 ++++ 6 files changed, 242 insertions(+), 108 deletions(-) diff --git a/apps/member/tables.py b/apps/member/tables.py index 8f5ceb88..6242fa3b 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -130,3 +130,31 @@ class MembershipTable(tables.Table): template_name = 'django_tables2/bootstrap4.html' fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', ) model = Membership + + +class ClubManagerTable(tables.Table): + """ + List managers of a club. + """ + + def render_user(self, value): + # If the user has the right, link the displayed user with the page of its detail. + s = value.username + if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value): + s = format_html("{name}", + url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s) + + return s + + def render_roles(self, record): + roles = record.roles.all() + return ", ".join(str(role) for role in roles) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover', + 'style': 'table-layout: fixed;' + } + template_name = 'django_tables2/bootstrap4.html' + fields = ('user', 'user.first_name', 'user.last_name', 'roles', ) + model = Membership diff --git a/apps/member/views.py b/apps/member/views.py index 1785d1ad..57fe29e7 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -30,7 +30,7 @@ from permission.views import ProtectQuerysetMixin from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm from .models import Club, Membership -from .tables import ClubTable, UserTable, MembershipTable +from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable class CustomLoginView(LoginView): @@ -338,6 +338,10 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): club.update_membership_dates() + managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\ + .order_by('user__last_name').all() + context["managers"] = ClubManagerTable(data=managers, prefix="managers-") + club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ .order_by('-created_at') @@ -672,21 +676,33 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern)) ) - if 'only_active' in self.request.GET: - only_active = self.request.GET["only_active"] != '0' - else: - only_active = True + only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' if only_active: qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today()) + if "roles" in self.request.GET: + if not self.request.GET["roles"]: + return qs.none() + roles_str = self.request.GET["roles"].replace(' ', '').split(',') + roles_int = map(int, roles_str) + qs = qs.filter(roles__in=roles_int) + qs = qs.order_by('-date_start', 'user__username') return qs.distinct() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["club"] = Club.objects.filter( + club = Club.objects.filter( PermissionBackend.filter_queryset(self.request.user, Club, "view") ).get(pk=self.kwargs["pk"]) + context["club"] = club + + applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub')) + & (Q(for_club__isnull=True) | Q(for_club=club))).all() + context["applicable_roles"] = applicable_roles + + context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' + return context \ No newline at end of file diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index c9ac11ba..f7b8412f 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-30 17:22+0200\n" +"POT-Creation-Date: 2020-07-31 17:00+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -186,12 +186,12 @@ msgstr "" msgid "Type" msgstr "" -#: apps/activity/tables.py:77 apps/member/forms.py:104 +#: apps/activity/tables.py:77 apps/member/forms.py:102 #: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "" -#: apps/activity/tables.py:79 apps/member/forms.py:109 +#: apps/activity/tables.py:79 apps/member/forms.py:107 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" @@ -261,12 +261,12 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 apps/note/tables.py:143 +#: apps/logs/models.py:61 apps/note/tables.py:161 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "" -#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:148 +#: apps/logs/models.py:62 apps/note/tables.py:138 apps/note/tables.py:166 #: apps/wei/tables.py:65 msgid "delete" msgstr "" @@ -295,39 +295,47 @@ msgstr "" msgid "member" msgstr "" -#: apps/member/forms.py:58 apps/member/views.py:81 +#: apps/member/forms.py:58 apps/member/views.py:82 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/forms.py:83 apps/registration/forms.py:44 +#: apps/member/forms.py:81 apps/registration/forms.py:44 msgid "Inscription paid by Société Générale" msgstr "" -#: apps/member/forms.py:85 apps/registration/forms.py:46 +#: apps/member/forms.py:83 apps/registration/forms.py:46 msgid "Check this case is the Société Générale paid the inscription." msgstr "" -#: apps/member/forms.py:90 apps/registration/forms.py:51 +#: apps/member/forms.py:88 apps/registration/forms.py:51 msgid "Credit type" msgstr "" -#: apps/member/forms.py:91 apps/registration/forms.py:52 +#: apps/member/forms.py:89 apps/registration/forms.py:52 msgid "No credit" msgstr "" -#: apps/member/forms.py:93 +#: apps/member/forms.py:91 msgid "You can credit the note of the user." msgstr "" -#: apps/member/forms.py:97 apps/registration/forms.py:57 +#: apps/member/forms.py:95 apps/registration/forms.py:57 msgid "Credit amount" msgstr "" -#: apps/member/forms.py:114 apps/registration/forms.py:74 +#: apps/member/forms.py:112 apps/registration/forms.py:74 #: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "" +#: apps/member/forms.py:138 +msgid "User" +msgstr "" + +#: apps/member/forms.py:152 +msgid "Roles" +msgstr "" + #: apps/member/models.py:34 #: templates/registration/future_profile_detail.html:40 #: templates/wei/weimembership_form.html:48 @@ -525,7 +533,7 @@ msgstr "" msgid "fee" msgstr "" -#: apps/member/models.py:303 apps/member/views.py:535 apps/wei/views.py:787 +#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:787 msgid "User is not a member of the parent club" msgstr "" @@ -534,7 +542,7 @@ msgstr "" msgid "The role {role} does not apply to the club {club}." msgstr "" -#: apps/member/models.py:321 apps/member/views.py:544 +#: apps/member/models.py:321 apps/member/views.py:543 msgid "User is already a member of the club" msgstr "" @@ -555,80 +563,84 @@ msgstr "" msgid "Renew" msgstr "" -#: apps/member/views.py:56 templates/member/profile_info.html:47 +#: apps/member/views.py:57 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:130 msgid "Update Profile" msgstr "" -#: apps/member/views.py:69 apps/registration/forms.py:23 +#: apps/member/views.py:70 apps/registration/forms.py:23 msgid "This address must be valid." msgstr "" -#: apps/member/views.py:126 +#: apps/member/views.py:127 msgid "Profile detail" msgstr "" -#: apps/member/views.py:160 +#: apps/member/views.py:161 msgid "Search user" msgstr "" -#: apps/member/views.py:194 apps/member/views.py:376 +#: apps/member/views.py:195 apps/member/views.py:381 msgid "Note aliases" msgstr "" -#: apps/member/views.py:208 +#: apps/member/views.py:209 msgid "Update note picture" msgstr "" -#: apps/member/views.py:266 templates/member/profile_info.html:43 +#: apps/member/views.py:267 templates/member/profile_info.html:43 msgid "Manage auth token" msgstr "" -#: apps/member/views.py:294 +#: apps/member/views.py:295 msgid "Create new club" msgstr "" -#: apps/member/views.py:306 +#: apps/member/views.py:307 msgid "Search club" msgstr "" -#: apps/member/views.py:331 +#: apps/member/views.py:332 msgid "Club detail" msgstr "" -#: apps/member/views.py:393 +#: apps/member/views.py:398 msgid "Update club" msgstr "" -#: apps/member/views.py:427 +#: apps/member/views.py:432 msgid "Add new member to the club" msgstr "" -#: apps/member/views.py:530 apps/wei/views.py:778 +#: apps/member/views.py:529 apps/wei/views.py:778 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" -#: apps/member/views.py:548 +#: apps/member/views.py:547 msgid "The membership must start after {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:553 +#: apps/member/views.py:552 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:570 apps/member/views.py:572 apps/member/views.py:574 +#: apps/member/views.py:569 apps/member/views.py:571 apps/member/views.py:573 #: apps/registration/views.py:295 apps/registration/views.py:297 #: apps/registration/views.py:299 msgid "This field is required." msgstr "" -#: apps/member/views.py:634 +#: apps/member/views.py:641 msgid "Manage roles of an user in the club" msgstr "" +#: apps/member/views.py:666 +msgid "Members of the club" +msgstr "" + #: apps/note/admin.py:121 apps/note/models/transactions.py:106 msgid "source" msgstr "" @@ -784,7 +796,7 @@ msgid "transaction templates" msgstr "" #: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 -#: apps/note/tables.py:33 apps/note/tables.py:42 +#: apps/note/tables.py:35 apps/note/tables.py:44 msgid "used alias" msgstr "" @@ -796,7 +808,7 @@ msgstr "" msgid "reason" msgstr "" -#: apps/note/models/transactions.py:151 apps/note/tables.py:95 +#: apps/note/models/transactions.py:151 apps/note/tables.py:113 msgid "invalidity reason" msgstr "" @@ -858,25 +870,25 @@ msgstr "" msgid "membership transactions" msgstr "" -#: apps/note/tables.py:57 +#: apps/note/tables.py:63 msgid "Click to invalidate" msgstr "" -#: apps/note/tables.py:57 +#: apps/note/tables.py:63 msgid "Click to validate" msgstr "" -#: apps/note/tables.py:93 +#: apps/note/tables.py:111 msgid "No reason specified" msgstr "" -#: apps/note/tables.py:122 apps/note/tables.py:150 apps/wei/tables.py:66 +#: apps/note/tables.py:140 apps/note/tables.py:168 apps/wei/tables.py:66 #: templates/treasury/sogecredit_detail.html:59 #: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "" -#: apps/note/tables.py:145 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: apps/note/tables.py:163 apps/wei/tables.py:42 apps/wei/tables.py:43 #: templates/member/club_info.html:67 templates/note/conso_form.html:128 #: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 #: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 @@ -1828,11 +1840,28 @@ msgstr "" msgid "Club listing" msgstr "" +#: templates/member/club_members.html:16 +msgid "Display only active memberships" +msgstr "" + +#: templates/member/club_members.html:21 +msgid "Filter roles:" +msgstr "" + +#: templates/member/club_members.html:37 +#: templates/wei/weimembership_list.html:18 +msgid "There is no membership found with this pattern." +msgstr "" + #: templates/member/club_tables.html:7 +msgid "Club managers" +msgstr "" + +#: templates/member/club_tables.html:20 msgid "Club members" msgstr "" -#: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 +#: templates/member/club_tables.html:33 templates/member/profile_tables.html:28 #: templates/wei/weiclub_tables.html:105 msgid "Transaction history" msgstr "" @@ -2469,10 +2498,6 @@ msgid "" " " msgstr "" -#: templates/wei/weimembership_list.html:18 -msgid "There is no membership found with this pattern." -msgstr "" - #: templates/wei/weimembership_list.html:24 msgid "View unvalidated registrations..." msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index c561abba..91c6f1f7 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-30 17:22+0200\n" +"POT-Creation-Date: 2020-07-31 17:00+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -187,12 +187,12 @@ msgstr "supprimer" msgid "Type" msgstr "Type" -#: apps/activity/tables.py:77 apps/member/forms.py:104 +#: apps/activity/tables.py:77 apps/member/forms.py:102 #: apps/registration/forms.py:64 apps/treasury/forms.py:120 msgid "Last name" msgstr "Nom de famille" -#: apps/activity/tables.py:79 apps/member/forms.py:109 +#: apps/activity/tables.py:79 apps/member/forms.py:107 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 #: templates/note/transaction_form.html:126 msgid "First name" @@ -262,12 +262,12 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 apps/note/tables.py:143 +#: apps/logs/models.py:61 apps/note/tables.py:161 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "Modifier" -#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:148 +#: apps/logs/models.py:62 apps/note/tables.py:138 apps/note/tables.py:166 #: apps/wei/tables.py:65 msgid "delete" msgstr "Supprimer" @@ -296,39 +296,47 @@ msgstr "journaux de modifications" msgid "member" msgstr "adhérent" -#: apps/member/forms.py:58 apps/member/views.py:81 +#: apps/member/forms.py:58 apps/member/views.py:82 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/forms.py:83 apps/registration/forms.py:44 +#: apps/member/forms.py:81 apps/registration/forms.py:44 msgid "Inscription paid by Société Générale" msgstr "Inscription payée par la Société générale" -#: apps/member/forms.py:85 apps/registration/forms.py:46 +#: apps/member/forms.py:83 apps/registration/forms.py:46 msgid "Check this case is the Société Générale paid the inscription." msgstr "Cochez cette case si la Société Générale a payé l'inscription." -#: apps/member/forms.py:90 apps/registration/forms.py:51 +#: apps/member/forms.py:88 apps/registration/forms.py:51 msgid "Credit type" msgstr "Type de rechargement" -#: apps/member/forms.py:91 apps/registration/forms.py:52 +#: apps/member/forms.py:89 apps/registration/forms.py:52 msgid "No credit" msgstr "Pas de rechargement" -#: apps/member/forms.py:93 +#: apps/member/forms.py:91 msgid "You can credit the note of the user." msgstr "Vous pouvez créditer la note de l'utisateur avant l'adhésion." -#: apps/member/forms.py:97 apps/registration/forms.py:57 +#: apps/member/forms.py:95 apps/registration/forms.py:57 msgid "Credit amount" msgstr "Montant à créditer" -#: apps/member/forms.py:114 apps/registration/forms.py:74 +#: apps/member/forms.py:112 apps/registration/forms.py:74 #: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 msgid "Bank" msgstr "Banque" +#: apps/member/forms.py:138 +msgid "User" +msgstr "Utilisateur" + +#: apps/member/forms.py:152 +msgid "Roles" +msgstr "Rôles" + #: apps/member/models.py:34 #: templates/registration/future_profile_detail.html:40 #: templates/wei/weimembership_form.html:48 @@ -530,7 +538,7 @@ msgstr "l'adhésion finit le" msgid "fee" msgstr "cotisation" -#: apps/member/models.py:303 apps/member/views.py:535 apps/wei/views.py:787 +#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:787 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" @@ -539,7 +547,7 @@ msgstr "L'utilisateur n'est pas membre du club parent" msgid "The role {role} does not apply to the club {club}." msgstr "Le rôle {role} ne s'applique pas au club {club}." -#: apps/member/models.py:321 apps/member/views.py:544 +#: apps/member/models.py:321 apps/member/views.py:543 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" @@ -560,57 +568,57 @@ msgstr "adhésions" msgid "Renew" msgstr "Renouveler" -#: apps/member/views.py:56 templates/member/profile_info.html:47 +#: apps/member/views.py:57 templates/member/profile_info.html:47 #: templates/registration/future_profile_detail.html:48 #: templates/wei/weimembership_form.html:130 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:69 apps/registration/forms.py:23 +#: apps/member/views.py:70 apps/registration/forms.py:23 msgid "This address must be valid." msgstr "Cette adresse doit être valide." -#: apps/member/views.py:126 +#: apps/member/views.py:127 msgid "Profile detail" msgstr "Détails de l'utilisateur" -#: apps/member/views.py:160 +#: apps/member/views.py:161 msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:194 apps/member/views.py:376 +#: apps/member/views.py:195 apps/member/views.py:381 msgid "Note aliases" msgstr "Alias de la note" -#: apps/member/views.py:208 +#: apps/member/views.py:209 msgid "Update note picture" msgstr "Modifier la photo de la note" -#: apps/member/views.py:266 templates/member/profile_info.html:43 +#: apps/member/views.py:267 templates/member/profile_info.html:43 msgid "Manage auth token" msgstr "Gérer les jetons d'authentification" -#: apps/member/views.py:294 +#: apps/member/views.py:295 msgid "Create new club" msgstr "Créer un nouveau club" -#: apps/member/views.py:306 +#: apps/member/views.py:307 msgid "Search club" msgstr "Chercher un club" -#: apps/member/views.py:331 +#: apps/member/views.py:332 msgid "Club detail" msgstr "Détails du club" -#: apps/member/views.py:393 +#: apps/member/views.py:398 msgid "Update club" msgstr "Modifier le club" -#: apps/member/views.py:427 +#: apps/member/views.py:432 msgid "Add new member to the club" msgstr "Ajouter un nouveau membre au club" -#: apps/member/views.py:530 apps/wei/views.py:778 +#: apps/member/views.py:529 apps/wei/views.py:778 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -618,24 +626,28 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:548 +#: apps/member/views.py:547 msgid "The membership must start after {:%m-%d-%Y}." msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." -#: apps/member/views.py:553 +#: apps/member/views.py:552 msgid "The membership must begin before {:%m-%d-%Y}." msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." -#: apps/member/views.py:570 apps/member/views.py:572 apps/member/views.py:574 +#: apps/member/views.py:569 apps/member/views.py:571 apps/member/views.py:573 #: apps/registration/views.py:295 apps/registration/views.py:297 #: apps/registration/views.py:299 msgid "This field is required." msgstr "Ce champ est requis." -#: apps/member/views.py:634 +#: apps/member/views.py:641 msgid "Manage roles of an user in the club" msgstr "Gérer les rôles d'un utilisateur dans le club" +#: apps/member/views.py:666 +msgid "Members of the club" +msgstr "Membres du club" + #: apps/note/admin.py:121 apps/note/models/transactions.py:106 msgid "source" msgstr "source" @@ -792,7 +804,7 @@ msgid "transaction templates" msgstr "modèles de transaction" #: apps/note/models/transactions.py:112 apps/note/models/transactions.py:125 -#: apps/note/tables.py:33 apps/note/tables.py:42 +#: apps/note/tables.py:35 apps/note/tables.py:44 msgid "used alias" msgstr "alias utilisé" @@ -804,7 +816,7 @@ msgstr "quantité" msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:151 apps/note/tables.py:95 +#: apps/note/models/transactions.py:151 apps/note/tables.py:113 msgid "invalidity reason" msgstr "Motif d'invalidité" @@ -870,25 +882,25 @@ msgstr "Transaction d'adhésion" msgid "membership transactions" msgstr "Transactions d'adhésion" -#: apps/note/tables.py:57 +#: apps/note/tables.py:63 msgid "Click to invalidate" msgstr "Cliquez pour dévalider" -#: apps/note/tables.py:57 +#: apps/note/tables.py:63 msgid "Click to validate" msgstr "Cliquez pour valider" -#: apps/note/tables.py:93 +#: apps/note/tables.py:111 msgid "No reason specified" msgstr "Pas de motif spécifié" -#: apps/note/tables.py:122 apps/note/tables.py:150 apps/wei/tables.py:66 +#: apps/note/tables.py:140 apps/note/tables.py:168 apps/wei/tables.py:66 #: templates/treasury/sogecredit_detail.html:59 #: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "Supprimer" -#: apps/note/tables.py:145 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: apps/note/tables.py:163 apps/wei/tables.py:42 apps/wei/tables.py:43 #: templates/member/club_info.html:67 templates/note/conso_form.html:128 #: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 #: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 @@ -1888,11 +1900,28 @@ msgstr "Créer un club" msgid "Club listing" msgstr "Liste des clubs" +#: templates/member/club_members.html:16 +msgid "Display only active memberships" +msgstr "N'afficher que les adhésions encore valides" + +#: templates/member/club_members.html:21 +msgid "Filter roles:" +msgstr "Filtrer par rôle :" + +#: templates/member/club_members.html:37 +#: templates/wei/weimembership_list.html:18 +msgid "There is no membership found with this pattern." +msgstr "Il n'y a pas d'adhésion trouvée avec cette entrée." + #: templates/member/club_tables.html:7 +msgid "Club managers" +msgstr "Bureau du club" + +#: templates/member/club_tables.html:20 msgid "Club members" msgstr "Membres du club" -#: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 +#: templates/member/club_tables.html:33 templates/member/profile_tables.html:28 #: templates/wei/weiclub_tables.html:105 msgid "Transaction history" msgstr "Historique des transactions" @@ -2591,10 +2620,6 @@ msgstr "" "l'inscription au WEI.\n" " " -#: templates/wei/weimembership_list.html:18 -msgid "There is no membership found with this pattern." -msgstr "Il n'y a pas d'adhésion trouvée avec cette entrée." - #: templates/wei/weimembership_list.html:24 msgid "View unvalidated registrations..." msgstr "Voir les inscriptions non validées ..." diff --git a/templates/member/club_members.html b/templates/member/club_members.html index fca6ec1c..ed2debe4 100644 --- a/templates/member/club_members.html +++ b/templates/member/club_members.html @@ -3,11 +3,30 @@ {% load render_table from django_tables2 %} {% block profile_info %} -{% include "member/club_info.html" %} + {% include "member/club_info.html" %} {% endblock %} {% block profile_content %} +
    +
    + +
    +
    +
    + +
    + +
    +

    @@ -22,21 +41,29 @@ {% endblock %} {% block extrajavascript %} - + searchbar_obj.keyup(reloadTable); + only_active_obj.change(reloadTable); + roles_obj.change(reloadTable); + }); + {% endblock %} diff --git a/templates/member/club_tables.html b/templates/member/club_tables.html index 92d66425..d4a9575b 100644 --- a/templates/member/club_tables.html +++ b/templates/member/club_tables.html @@ -1,5 +1,18 @@ {% load render_table from django_tables2 %} {% load i18n %} +{% if managers.data %} +
    + + {% render_table managers %} +
    + +
    +{% endif %} + {% if member_list.data %}
    From 9008baad3a40d20d37fc89ec763f36365aa3efb8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 17:07:14 +0200 Subject: [PATCH 63/77] Better alias research, closes #51 --- apps/note/api/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 8a1616ce..a365c343 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -106,7 +106,10 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): queryset = super().get_queryset() alias = self.request.query_params.get("alias", ".*") - queryset = queryset.filter(normalized_name__iregex="^" + Alias.normalize(alias))\ + queryset = queryset.filter( + Q(name__regex="^" + alias) + | Q(normalized_name__regex="^" + Alias.normalize(alias)) + | Q(normalized_name__regex="^" + alias.lower()))\ .order_by('name').prefetch_related('note') return queryset From 8409ee4cc454328d5fb0071c6862244cb6a4ff44 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 17:15:31 +0200 Subject: [PATCH 64/77] Display "Create WEI" button only for people that can create a WEI, see #53 --- apps/wei/views.py | 5 +++++ templates/wei/weiclub_list.html | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/wei/views.py b/apps/wei/views.py index dde9fe78..f2ac40dc 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -54,6 +54,11 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): ordering = '-year' extra_context = {"title": _("Search WEI")} + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub()) + return context + class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ diff --git a/templates/wei/weiclub_list.html b/templates/wei/weiclub_list.html index 5390ade6..2739e5bf 100644 --- a/templates/wei/weiclub_list.html +++ b/templates/wei/weiclub_list.html @@ -5,8 +5,10 @@
    -
    - {% trans "Create WEI" %} + {% if can_create_wei %} +
    + {% trans "Create WEI" %} + {% endif %}
    From d8127e8936f4143992e8e0f084759bbcebb586d3 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 19:00:19 +0200 Subject: [PATCH 65/77] Escape strings --- templates/note/conso_form.html | 8 ++++---- templates/note/transaction_form.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html index 005e3e05..e6335c6e 100644 --- a/templates/note/conso_form.html +++ b/templates/note/conso_form.html @@ -161,8 +161,8 @@ {% if button.display %} $("#highlighted_button{{ button.id }}").click(function() { addConso({{ button.destination_id }}, {{ button.amount }}, - {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name }}", - {{ button.id }}, "{{ button.name }}"); + {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", + {{ button.id }}, "{{ button.name|escapejs }}"); }); {% endif %} {% endfor %} @@ -172,8 +172,8 @@ {% if button.display %} $("#button{{ button.id }}").click(function() { addConso({{ button.destination_id }}, {{ button.amount }}, - {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name }}", - {{ button.id }}, "{{ button.name }}"); + {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", + {{ button.id }}, "{{ button.name|escapejs }}"); }); {% endif %} {% endfor %} diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index fd3e5406..cb7df094 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -160,7 +160,7 @@ SPDX-License-Identifier: GPL-2.0-or-later TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }}; SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }}; user_id = {{ user.note.pk }}; - username = "{{ user.username }}"; + username = "{{ user.username|escapejs }}"; {% endblock %} From 932a5462134584d6c70f22dc97331e73dab3876f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 19:43:03 +0200 Subject: [PATCH 66/77] Better error messages --- static/js/base.js | 2 +- static/js/consos.js | 4 ++-- static/js/transfer.js | 15 +++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/static/js/base.js b/static/js/base.js index 77057103..fdad58d7 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -369,7 +369,7 @@ function de_validate(id, validated) { }, error: function (err) { addMsg("Une erreur est survenue lors de la validation/dévalidation " + - "de cette transaction : " + err.responseText, "danger"); + "de cette transaction : " + JSON.parse(err.responseText)["detail"], "danger", 10000); refreshBalance(); // error if this method doesn't exist. Please define it. diff --git a/static/js/consos.js b/static/js/consos.js index 2fa77249..268ddc95 100644 --- a/static/js/consos.js +++ b/static/js/consos.js @@ -207,10 +207,10 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat "template": template }).done(function() { reset(); - addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger"); + addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000); }).fail(function () { reset(); - errMsg(e.responseJSON); + errMsg(e.responseJSON, 10000); }); }); } diff --git a/static/js/transfer.js b/static/js/transfer.js index ed651640..4234e775 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -189,7 +189,7 @@ $("#btn_transfer").click(function() { }).done(function () { addMsg("Le transfert de " + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name - + " vers la note " + dest.name + " a été fait avec succès !", "success"); + + " vers la note " + dest.name + " a été fait avec succès !", "success", 10000); reset(); }).fail(function (err) { // do it again but valid = false @@ -210,15 +210,11 @@ $("#btn_transfer").click(function() { }).done(function () { addMsg("Le transfert de " + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger"); - - reset(); + + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000); }).fail(function (err) { addMsg("Le transfert de " + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); - - reset(); + + " vers la note " + dest.name + " a échoué : " + JSON.parse(err.responseText)["detail"], "danger", 10000); }); }); }); @@ -261,11 +257,10 @@ $("#btn_transfer").click(function() { "first_name": $("#first_name").val(), "bank": $("#bank").val() }).done(function () { - addMsg("Le crédit/retrait a bien été effectué !", "success"); + addMsg("Le crédit/retrait a bien été effectué !", "success", 10000); reset(); }).fail(function (err) { - addMsg("Le crédit/retrait a échoué : " + err.responseText, "danger"); - reset(); + addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"], "danger", 10000); }); } }); From dca655949e1db3d740bbd47eba32aaee44bcb8b1 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 21:24:23 +0200 Subject: [PATCH 67/77] Improve transfer UI --- apps/permission/models.py | 9 ++++ apps/wei/views.py | 7 ++- static/js/transfer.js | 68 ++++++++++++++++++++++------ templates/note/amount_input.html | 1 + templates/note/transaction_form.html | 3 +- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/apps/permission/models.py b/apps/permission/models.py index 79148fe4..beace783 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -4,6 +4,7 @@ import functools import json import operator +from time import sleep from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError @@ -44,6 +45,14 @@ class InstancedPermission: else: oldpk = obj.pk # Ensure previous models are deleted + count = 0 + while count < 1000: + if self.model.model_class().objects.filter(pk=obj.pk).exists(): + # If the object exists, that means that one permission is currently checked. + # We wait before the other permission, at most 1 second. + sleep(1) + continue + break for o in self.model.model_class().objects.filter(pk=obj.pk).all(): o._force_delete = True Model.delete(o) diff --git a/apps/wei/views.py b/apps/wei/views.py index f2ac40dc..2210a347 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -17,6 +17,7 @@ from django.http import HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse_lazy +from django.utils import timezone from django.views import View from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView from django.utils.translation import gettext_lazy as _ @@ -56,7 +57,11 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub()) + context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub( + year=0, + date_start=timezone.now().date(), + date_end=timezone.now().date(), + )) return context diff --git a/static/js/transfer.js b/static/js/transfer.js index 4234e775..ba0bf9e3 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -14,8 +14,14 @@ function reset(refresh=true) { dests.length = 0; $("#source_note_list").html(""); $("#dest_note_list").html(""); - $("#amount").val(""); - $("#reason").val(""); + let amount_field = $("#amount"); + amount_field.val(""); + amount_field.removeClass('is-invalid'); + $("#amount-required").html(""); + let reason_field = $("#reason"); + reason_field.val(""); + reason_field.removeClass('is-invalid'); + $("#reason-required").html(""); $("#last_name").val(""); $("#first_name").val(""); $("#bank").val(""); @@ -140,7 +146,9 @@ $(document).ready(function() { $("#source_me").click(function() { // Shortcut to set the current user as the only emitter - reset(false); + sources_notes_display.length = 0; + sources.length = 0; + $("#source_note_list").html(""); let source_note = $("#source_note"); source_note.focus(); @@ -170,15 +178,45 @@ $(document).ready(function() { }); $("#btn_transfer").click(function() { + let error = false; + + let amount_field = $("#amount"); + amount_field.removeClass('is-invalid'); + $("#amount-required").html(""); + + let reason_field = $("#reason"); + reason_field.removeClass('is-invalid'); + $("#reason-required").html(""); + + if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { + amount_field.addClass('is-invalid'); + $("#amount-required").html("Ce champ est requis et doit comporter un nombre décimal strictement positif."); + error = true; + } + + if (!reason_field.val()) { + reason_field.addClass('is-invalid'); + $("#reason-required").html("Ce champ est requis."); + error = true; + } + + if (error) + return; + + let amount = 100 * amount_field.val(); + let reason = reason_field.val(); + if ($("#type_transfer").is(':checked')) { - sources_notes_display.forEach(function (source) { - dests_notes_display.forEach(function (dest) { + + // We copy the arrays to ensure that transactions are well-processed even if the form is reset + [...sources_notes_display].forEach(function (source) { + [...dests_notes_display].forEach(function (dest) { $.post("/api/note/transaction/transaction/", { "csrfmiddlewaretoken": CSRF_TOKEN, "quantity": source.quantity * dest.quantity, - "amount": 100 * $("#amount").val(), - "reason": $("#reason").val(), + "amount": amount, + "reason": reason, "valid": true, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", @@ -188,7 +226,7 @@ $("#btn_transfer").click(function() { "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name + " vers la note " + dest.name + " a été fait avec succès !", "success", 10000); reset(); @@ -197,8 +235,8 @@ $("#btn_transfer").click(function() { { "csrfmiddlewaretoken": CSRF_TOKEN, "quantity": source.quantity * dest.quantity, - "amount": 100 * $("#amount").val(), - "reason": $("#reason").val(), + "amount": amount, + "reason": reason, "valid": false, "invalidity_reason": "Solde insuffisant", "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, @@ -209,12 +247,12 @@ $("#btn_transfer").click(function() { "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000); }).fail(function (err) { addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : " + JSON.parse(err.responseText)["detail"], "danger", 10000); + + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name + + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); }); }); }); @@ -222,7 +260,7 @@ $("#btn_transfer").click(function() { } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { let special_note = $("#credit_type").val(); let user_note; - let given_reason = $("#reason").val(); + let given_reason = reason; let source, dest, reason; if ($("#type_credit").is(':checked')) { user_note = dests_notes_display[0].note.id; @@ -244,7 +282,7 @@ $("#btn_transfer").click(function() { { "csrfmiddlewaretoken": CSRF_TOKEN, "quantity": 1, - "amount": 100 * $("#amount").val(), + "amount": amount, "reason": reason, "valid": true, "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, diff --git a/templates/note/amount_input.html b/templates/note/amount_input.html index 6ef4a53a..cd8cd201 100644 --- a/templates/note/amount_input.html +++ b/templates/note/amount_input.html @@ -8,4 +8,5 @@
    +

    \ No newline at end of file diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index cb7df094..b761490d 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -100,7 +100,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
    - + +

    From d7b834d9089e647084944b37ae4e24f3a03e5a72 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Fri, 31 Jul 2020 22:29:23 +0200 Subject: [PATCH 68/77] Translate rights --- apps/note/models/transactions.py | 2 + apps/permission/fixtures/initial.json | 400 ++++++++++++-------------- apps/permission/models.py | 8 +- locale/de/LC_MESSAGES/django.po | 154 +++++----- locale/fr/LC_MESSAGES/django.po | 154 +++++----- templates/base.html | 8 +- templates/permission/all_rights.html | 2 +- 7 files changed, 358 insertions(+), 370 deletions(-) diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 8a7162c3..f504d8e1 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -208,7 +208,9 @@ class Transaction(PolymorphicModel): super().save(*args, **kwargs) # Save notes + self.source._force_save = True self.source.save() + self.destination._force_save = True self.destination.save() def delete(self, **kwargs): diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 2622ed1c..5a3001da 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -36,7 +36,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our User object" + "description": "Voir son compte utilisateur" } }, { @@ -52,7 +52,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our profile" + "description": "Voir son profil" } }, { @@ -68,7 +68,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our own note" + "description": "Vioir sa propre note d'utilisateur" } }, { @@ -84,7 +84,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our API token" + "description": "Voir son jeton d'authentification à l'API" } }, { @@ -100,7 +100,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our own transactions" + "description": "Voir ses propres transactions" } }, { @@ -116,7 +116,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View aliases of clubs and members of Kfet club" + "description": "Voir les aliases des notes des clubs et des adhérents du club Kfet" } }, { @@ -132,7 +132,7 @@ "mask": 1, "field": "last_login", "permanent": true, - "description": "Change myself's last login" + "description": "Modifier sa propre date de dernière connexion" } }, { @@ -148,7 +148,7 @@ "mask": 1, "field": "username", "permanent": true, - "description": "Change myself's username" + "description": "Changer son propre pseudo" } }, { @@ -164,7 +164,7 @@ "mask": 1, "field": "first_name", "permanent": true, - "description": "Change myself's first name" + "description": "Changer son propre prénom" } }, { @@ -180,7 +180,7 @@ "mask": 1, "field": "last_name", "permanent": true, - "description": "Change myself's last name" + "description": "Changer son propre nom de famille" } }, { @@ -196,7 +196,7 @@ "mask": 1, "field": "email", "permanent": true, - "description": "Change myself's email" + "description": "Changer sa propre adresse e-mail" } }, { @@ -212,7 +212,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "Delete API Token" + "description": "Supprimer son jeton d'authentification à l'API" } }, { @@ -228,7 +228,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "Create API Token" + "description": "Créer un jeton d'authentification à l'API" } }, { @@ -244,7 +244,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Remove alias" + "description": "Supprimer un alias à sa note" } }, { @@ -260,7 +260,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add alias" + "description": "Ajouter un alias à sa note" } }, { @@ -276,7 +276,7 @@ "mask": 1, "field": "display_image", "permanent": false, - "description": "Change myself's display image" + "description": "Changer l'image de sa note" } }, { @@ -292,23 +292,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Transfer from myself's note" - } - }, - { - "model": "permission.permission", - "pk": 18, - "fields": { - "model": [ - "note", - "note" - ], - "query": "{}", - "type": "change", - "mask": 1, - "field": "balance", - "permanent": false, - "description": "Update a note balance with a transaction" + "description": "Transférer de l'argent depuis sa propre note en restant positif" } }, { @@ -324,7 +308,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View notes of club members" + "description": "Voir les notes des membres du club" } }, { @@ -340,7 +324,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Create transactions with a club" + "description": "Créer une transaction de ou vers la note d'un club" } }, { @@ -356,7 +340,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Create transactions from buttons with a club" + "description": "Créer une transaction en appuyant sur un bouton lié à un club" } }, { @@ -372,7 +356,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View club infos" + "description": "Voir les informations d'un club" } }, { @@ -388,7 +372,7 @@ "mask": 1, "field": "valid", "permanent": false, - "description": "Update validation status of a transaction" + "description": "Mettre à jour le statut de validation d'une transaction" } }, { @@ -404,7 +388,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View all transactions" + "description": "Voir toutes les transactions" } }, { @@ -420,7 +404,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Display credit/debit interface" + "description": "Afficher l'interface crédit/retrait" } }, { @@ -436,7 +420,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Create credit/debit transaction" + "description": "Créer un crédit ou un retrait quelconque" } }, { @@ -452,7 +436,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View button categories" + "description": "Voir toutes les catégories de boutons" } }, { @@ -468,7 +452,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Change button category" + "description": "Changer une catégorie de boutons" } }, { @@ -484,7 +468,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Add button category" + "description": "Créer une catégorie de boutons" } }, { @@ -500,7 +484,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View buttons" + "description": "Voir tous les boutons" } }, { @@ -516,7 +500,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Add buttons" + "description": "Ajouter un bouton" } }, { @@ -532,7 +516,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Update buttons" + "description": "Modifier un bouton" } }, { @@ -548,7 +532,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Create any transaction" + "description": "Créer n'importe quelle transaction" } }, { @@ -564,7 +548,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View valid activites" + "description": "Voir toutes les activités valides" } }, { @@ -580,7 +564,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Change our activities" + "description": "Modifier les activités non validées dont on est l'auteur" } }, { @@ -596,7 +580,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add activities" + "description": "Proposer des activités" } }, { @@ -612,7 +596,7 @@ "mask": 2, "field": "valid", "permanent": false, - "description": "Validate activities" + "description": "Valider des activités" } }, { @@ -628,7 +612,7 @@ "mask": 2, "field": "open", "permanent": false, - "description": "Open activities" + "description": "Ouvrir des activités" } }, { @@ -644,7 +628,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Invite people to activities" + "description": "Inviter des personnes à des activités" } }, { @@ -660,7 +644,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View invited people" + "description": "Voir les personnes qu'on a invitées" } }, { @@ -676,7 +660,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View all activities" + "description": "Voir toutes les activités" } }, { @@ -692,7 +676,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "View all invited people" + "description": "Voir toutes les personnes invitées" } }, { @@ -703,12 +687,12 @@ "activity", "entry" ], - "query": "{}", + "query": "{\"activity__open\": true}", "type": "add", "mask": 2, "field": "", "permanent": false, - "description": "Manage entries" + "description": "Gérer les entrées d'une activité ouverte" } }, { @@ -724,7 +708,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Add invitation transactions" + "description": "Créer une transaction d'invitation" } }, { @@ -740,7 +724,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View invitation transactions" + "description": "Voir toutes les transactions d'invitation" } }, { @@ -756,7 +740,7 @@ "mask": 2, "field": "valid", "permanent": false, - "description": "Validate invitation transactions" + "description": "Valider les transactions d'invitation" } }, { @@ -772,7 +756,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Update club" + "description": "Modifier un club" } }, { @@ -788,7 +772,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View our memberships" + "description": "Voir mes adhésions" } }, { @@ -804,7 +788,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View club's memberships" + "description": "Voir les adhérents du club" } }, { @@ -820,7 +804,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Add a membership to a club" + "description": "Ajouter un membre à un club" } }, { @@ -836,7 +820,7 @@ "mask": 3, "field": "roles", "permanent": false, - "description": "Update user roles" + "description": "Modifier les rôles d'un adhérent d'un club" } }, { @@ -852,7 +836,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Change own profile" + "description": "Modifier son profil" } }, { @@ -865,10 +849,10 @@ ], "query": "{}", "type": "change", - "mask": 2, + "mask": 3, "field": "", "permanent": false, - "description": "Change any profile" + "description": "Modifier n'importe quel profil" } }, { @@ -881,10 +865,10 @@ ], "query": "{}", "type": "change", - "mask": 2, + "mask": 3, "field": "", "permanent": false, - "description": "Change any user" + "description": "Modifier n'import quel utilisateur" } }, { @@ -900,7 +884,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add user" + "description": "Ajouter un utilisateur" } }, { @@ -916,7 +900,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add profile" + "description": "Ajouter un profil" } }, { @@ -929,10 +913,10 @@ ], "query": "{\"profile__registration_valid\": false}", "type": "delete", - "mask": 2, + "mask": 3, "field": "", "permanent": false, - "description": "Delete pre-registered user" + "description": "Supprimer une pré-inscription" } }, { @@ -945,10 +929,10 @@ ], "query": "{\"registration_valid\": false}", "type": "delete", - "mask": 2, + "mask": 3, "field": "", "permanent": false, - "description": "Delete pre-registered user profile" + "description": "Supprimer le profil d'une pré-inscription" } }, { @@ -964,7 +948,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "New club button" + "description": "Voir les boutons d'un club" } }, { @@ -977,10 +961,10 @@ ], "query": "{\"destination\": [\"club\", \"note\"]}", "type": "add", - "mask": 2, + "mask": 3, "field": "", "permanent": false, - "description": "Create club button" + "description": "Créer un bouton d'un club" } }, { @@ -996,7 +980,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Update club button" + "description": "Modifier le bouton d'un club" } }, { @@ -1012,7 +996,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View transactions of a club" + "description": "Voir les transactions d'un club" } }, { @@ -1028,7 +1012,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View invoices" + "description": "Voir les factures" } }, { @@ -1044,7 +1028,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Add invoice" + "description": "Ajouter une facture" } }, { @@ -1060,7 +1044,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Change invoice" + "description": "Modifier une facture" } }, { @@ -1076,7 +1060,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View products" + "description": "Voir les produits" } }, { @@ -1092,7 +1076,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Add products" + "description": "Ajouter des produits" } }, { @@ -1108,7 +1092,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Change product" + "description": "Modifier un produit" } }, { @@ -1124,7 +1108,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Delete product" + "description": "Supprimer un produit" } }, { @@ -1140,7 +1124,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + "description": "Ajouter un crédit de la Soci\u00e9t\u00e9 g\u00e9n\u00e9rale" } }, { @@ -1156,7 +1140,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View all Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credits" + "description": "Voir tous les crédits de la Soci\u00e9t\u00e9 g\u00e9n\u00e9rale" } }, { @@ -1172,7 +1156,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Update Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + "description": "Modifier un crédit de la Soci\u00e9t\u00e9 g\u00e9n\u00e9rale" } }, { @@ -1188,7 +1172,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Delete Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + "description": "Supprimer un crédit de la Soci\u00e9t\u00e9 g\u00e9n\u00e9rale" } }, { @@ -1204,7 +1188,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Create a WEI" + "description": "Créer un WEI" } }, { @@ -1220,7 +1204,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Update all WEI" + "description": "Modifier tous les WEI" } }, { @@ -1236,7 +1220,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Update this WEI" + "description": "Modifier ce WEI" } }, { @@ -1252,7 +1236,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View my WEI" + "description": "Voir mon WEI" } }, { @@ -1268,7 +1252,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View last WEI" + "description": "Voir le dernier WEI" } }, { @@ -1284,55 +1268,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View WEI Roles" - } - }, - { - "model": "permission.permission", - "pk": 80, - "fields": { - "model": [ - "wei", - "weirole" - ], - "query": "{}", - "type": "add", - "mask": 3, - "field": "", - "permanent": false, - "description": "Add WEI Role" - } - }, - { - "model": "permission.permission", - "pk": 81, - "fields": { - "model": [ - "wei", - "weirole" - ], - "query": "{}", - "type": "change", - "mask": 3, - "field": "", - "permanent": false, - "description": "Change WEI Role" - } - }, - { - "model": "permission.permission", - "pk": 82, - "fields": { - "model": [ - "wei", - "weirole" - ], - "query": "{}", - "type": "delete", - "mask": 3, - "field": "", - "permanent": false, - "description": "Delete WEI Role" + "description": "Voir les rôles pour le WEI" } }, { @@ -1348,7 +1284,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Register myself to the last WEI" + "description": "M'inscrire au dernier WEI" } }, { @@ -1364,7 +1300,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Register first year members to the last WEI" + "description": "Inscrire un 1A au WEI" } }, { @@ -1380,7 +1316,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Register anyone to this WEI" + "description": "Inscrire n'importe qui au WEI" } }, { @@ -1396,7 +1332,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Delete WEI registration" + "description": "Supprimer une inscription WEI" } }, { @@ -1412,7 +1348,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "View my own WEI registration" + "description": "Voir ma propre inscription WEI" } }, { @@ -1428,7 +1364,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View all WEI Registrations" + "description": "Voir toutes les inscriptions WEI" } }, { @@ -1444,7 +1380,7 @@ "mask": 1, "field": "soge_credit", "permanent": false, - "description": "Update the soge credit field of any WEI Registration" + "description": "Indiquer si une inscription WEI est payée par la Société générale" } }, { @@ -1460,7 +1396,7 @@ "mask": 1, "field": "soge_credit", "permanent": false, - "description": "Update the soge credit field of my own WEI Registration" + "description": "Indiquer si mon inscription WEI est payée par la Société générale tant qu'elle n'est pas validée" } }, { @@ -1476,7 +1412,7 @@ "mask": 1, "field": "caution_check", "permanent": false, - "description": "Update the caution check field of any WEI Registration" + "description": "Dire si un chèque de caution est donné pour une inscription WEI" } }, { @@ -1492,7 +1428,7 @@ "mask": 1, "field": "birth_date", "permanent": false, - "description": "Update the birth date of any WEI Registration" + "description": "Modifier la date de naissance d'une inscription WEI" } }, { @@ -1508,7 +1444,7 @@ "mask": 1, "field": "birth_date", "permanent": false, - "description": "Update the birth date of my own WEI Registration" + "description": "Modifier la date de naissance de ma propre inscription WEI" } }, { @@ -1524,7 +1460,7 @@ "mask": 1, "field": "gender", "permanent": false, - "description": "Update the gender of any WEI Registration" + "description": "Modifier le genre de toute inscription WEI" } }, { @@ -1540,7 +1476,7 @@ "mask": 1, "field": "gender", "permanent": false, - "description": "Update the gender of my own WEI Registration" + "description": "Modifier le genre de ma propre inscription WEI" } }, { @@ -1556,7 +1492,7 @@ "mask": 1, "field": "health_issues", "permanent": false, - "description": "Update the health issues of any WEI Registration" + "description": "Modifier les problèmes de santé de toutes les inscriptions WEI" } }, { @@ -1572,7 +1508,7 @@ "mask": 1, "field": "health_issues", "permanent": false, - "description": "Update the health issues of my own WEI Registration" + "description": "Modifier mes problèmes de santé de mon inscription WEI" } }, { @@ -1588,7 +1524,7 @@ "mask": 1, "field": "emergency_contact_name", "permanent": false, - "description": "Update the emergency contact name of any WEI Registration" + "description": "Modifier le nom du contact en cas d'urgence de toute inscription WEI" } }, { @@ -1604,7 +1540,7 @@ "mask": 1, "field": "emergency_contact_name", "permanent": false, - "description": "Update the emergency contact name of my own WEI Registration" + "description": "Modifier le nom du contact en cas d'urgence de mon inscription WEI" } }, { @@ -1620,7 +1556,7 @@ "mask": 1, "field": "emergency_contact_phone", "permanent": false, - "description": "Update the emergency contact phone of any WEI Registration" + "description": "Modifier le téléphone du contact en cas d'urgence de toute inscription WEI" } }, { @@ -1636,7 +1572,7 @@ "mask": 1, "field": "emergency_contact_phone", "permanent": false, - "description": "Update the emergency contact phone of my own WEI Registration" + "description": "Modifier le nom du contact en cas d'urgence de mon inscription WEI" } }, { @@ -1652,7 +1588,7 @@ "mask": 1, "field": "information_json", "permanent": false, - "description": "Update information of any WEI registration" + "description": "Modifier les informations (sondage 1A, ...) d'une inscription WEI" } }, { @@ -1668,7 +1604,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Add a bus for the current WEI" + "description": "Ajouter un bus au WEI" } }, { @@ -1684,7 +1620,7 @@ "mask": 3, "field": "name", "permanent": false, - "description": "Update the name of a bus for the last WEI" + "description": "Modifier le nom d'un bus d'un WEI" } }, { @@ -1700,7 +1636,7 @@ "mask": 3, "field": "description", "permanent": false, - "description": "Update the description of a bus for the last WEI" + "description": "Modifier la description d'un bus d'un WEI" } }, { @@ -1716,7 +1652,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Create a bus team for the last WEI" + "description": "Créer une équipe WEI" } }, { @@ -1732,7 +1668,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Update a bus team for the last WEI" + "description": "Modifier une équipe WEI" } }, { @@ -1748,7 +1684,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View buses of the last WEI" + "description": "Voir tous les bus WEI si on est en 2A+ ou que le WEI est terminé" } }, { @@ -1764,7 +1700,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View bus teams of the last WEI" + "description": "Voir toutes les équipes WEI si on est en 2A+ ou que le WEI est terminé" } }, { @@ -1780,7 +1716,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Create a WEI membership for the last WEI" + "description": "Créer une adhésion WEI pour le dernier WEI" } }, { @@ -1796,7 +1732,7 @@ "mask": 1, "field": "bus", "permanent": false, - "description": "Update the bus of a WEI membership" + "description": "Modifier le bus d'une adhésion WEI" } }, { @@ -1812,7 +1748,7 @@ "mask": 1, "field": "team", "permanent": false, - "description": "Update the team of a WEI membership" + "description": "Modifier l'équipe d'une adhésion WEI" } }, { @@ -1828,7 +1764,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View all WEI Memberships for the last WEI" + "description": "Voir toutes les adhésions au WEI" } }, { @@ -1839,12 +1775,12 @@ "wei", "weimembership" ], - "query": "[\"AND\", {\"user\": [\"user\"], \"club\": [\"club\"]}, [\"OR\", {\"registration__first_year\": false, \"club__weiclub__date_end__lte\": [\"today\"]}]]", + "query": "[\"AND\", {\"user\": [\"user\"], \"club\": [\"club\"]}, [\"OR\", {\"registration__first_year\": false}, {\"club__weiclub__date_end__lte\": [\"today\"]}]]", "type": "view", "mask": 1, "field": "", "permanent": true, - "description": "View my own WEI membership if I am an old member or if the WEI is past" + "description": "Voir mes adhésions WEI passées" } }, { @@ -1860,7 +1796,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View the members of the bus" + "description": "Voir les membres du bus" } }, { @@ -1876,7 +1812,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View the members of the team" + "description": "Voir les membres de l'équipe" } }, { @@ -1892,7 +1828,7 @@ "mask": 1, "field": "name", "permanent": false, - "description": "Update the name of my bus" + "description": "Modifier le nom du bus" } }, { @@ -1908,7 +1844,7 @@ "mask": 1, "field": "description", "permanent": false, - "description": "Update the description of my bus" + "description": "Modifier la description du bus" } }, { @@ -1924,7 +1860,7 @@ "mask": 1, "field": "", "permanent": false, - "description": "Add a team to my bus" + "description": "Ajouter une équipe à mon bus" } }, { @@ -1940,7 +1876,7 @@ "mask": 1, "field": "name", "permanent": false, - "description": "Update the name of a team of my bus" + "description": "Modifier le nom d'une équipe de mon bus" } }, { @@ -1956,7 +1892,7 @@ "mask": 1, "field": "color", "permanent": false, - "description": "Update the color of a team of my bus" + "description": "Modifier la couleur d'une équipe de mon bus" } }, { @@ -1972,7 +1908,7 @@ "mask": 1, "field": "description", "permanent": false, - "description": "Update the description of a team of my bus" + "description": "Modifier la description d'une équipe de mon bus" } }, { @@ -1988,7 +1924,7 @@ "mask": 1, "field": "name", "permanent": false, - "description": "Update the name of my team" + "description": "Modifier le nom de mon équipe" } }, { @@ -2004,7 +1940,7 @@ "mask": 1, "field": "color", "permanent": false, - "description": "Update the color of my team" + "description": "Modifier la couleur de mon équipe" } }, { @@ -2020,7 +1956,7 @@ "mask": 1, "field": "description", "permanent": false, - "description": "Update the description of my team" + "description": "Modifier la description de mon équipe" } }, { @@ -2036,7 +1972,7 @@ "mask": 1, "field": "", "permanent": true, - "description": "View my past activities" + "description": "Voir mes activitées passées, même après la fin de l'adhésion BDE" } }, { @@ -2049,10 +1985,10 @@ ], "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": true}]]", "type": "change", - "mask": 1, + "mask": 2, "field": "valid", "permanent": false, - "description": "Update validation status of a club transaction if possible" + "description": "Modifier le statut de validation d'une transaction de club si c'est possible" } }, { @@ -2068,7 +2004,7 @@ "mask": 1, "field": "clothing_cut", "permanent": false, - "description": "Update the clothing cut field of any WEI Registration" + "description": "Modifier la coupe de vêtements d'une inscription WEI" } }, { @@ -2084,7 +2020,7 @@ "mask": 1, "field": "clothing_cut", "permanent": false, - "description": "Update the clothing cut field of my own WEI Registration" + "description": "Modifier ma coupe de vêtements de mon inscription WEI" } }, { @@ -2100,7 +2036,7 @@ "mask": 1, "field": "clothing_size", "permanent": false, - "description": "Update the clothing size field of any WEI Registration" + "description": "Modifier la taille de vêtements d'une inscription WEI" } }, { @@ -2116,7 +2052,7 @@ "mask": 1, "field": "clothing_size", "permanent": false, - "description": "Update the clothing size field of my own WEI Registration" + "description": "Modifier la taille de vêtements de mon inscription WEI" } }, { @@ -2132,7 +2068,7 @@ "mask": 2, "field": "", "permanent": false, - "description": "Create any recurrent transaction" + "description": "Créer une transaction depuis un bouton" } }, { @@ -2148,7 +2084,7 @@ "mask": 1, "field": "invalidity_reason", "permanent": false, - "description": "Update invalidity reason of a club transaction if possible" + "description": "Modifier la raison d'invalidité d'une transaction de club" } }, { @@ -2164,7 +2100,7 @@ "mask": 1, "field": "invalidity_reason", "permanent": false, - "description": "Update invalidity reason of a transaction" + "description": "Modifier la raison d'invalidité d'une transaction" } }, { @@ -2180,7 +2116,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View any user" + "description": "Voir n'importe quel utilisateur" } }, { @@ -2196,7 +2132,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View any profile" + "description": "Voir n'importe quel profil" } }, { @@ -2212,7 +2148,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View any club" + "description": "Voir n'importe quel club" } }, { @@ -2228,7 +2164,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Change any club" + "description": "Modifier n'importe quel club" } }, { @@ -2244,7 +2180,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Create any noteclub" + "description": "Créer une note de club" } }, { @@ -2260,7 +2196,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "Create any club" + "description": "Créer un club" } }, { @@ -2276,7 +2212,7 @@ "mask": 3, "field": "", "permanent": false, - "description": "View members of our club" + "description": "Voir les membres de mon club" } }, { @@ -2292,7 +2228,23 @@ "mask": 2, "field": "", "permanent": false, - "description": "View club note" + "description": "Voir la note de mon club" + } + }, + { + "model": "permission.permission", + "pk": 143, + "fields": { + "model": [ + "note", + "noteuser" + ], + "query": "{}", + "type": "add", + "mask": 1, + "field": "", + "permanent": false, + "description": "Créer une note d'utilisateur" } }, { @@ -2338,7 +2290,6 @@ 15, 16, 17, - 18, 78, 79, 83, @@ -2468,7 +2419,8 @@ 136, 137, 138, - 139 + 139, + 143 ] } }, @@ -2496,7 +2448,6 @@ 15, 16, 17, - 18, 19, 20, 21, @@ -2558,9 +2509,6 @@ 77, 78, 79, - 80, - 81, - 82, 83, 84, 85, @@ -2620,7 +2568,8 @@ 139, 140, 141, - 142 + 142, + 143 ] } }, @@ -2648,7 +2597,8 @@ 28, 29, 30, - 31 + 31, + 143 ] } }, @@ -2678,9 +2628,6 @@ "name": "GC WEI", "permissions": [ 76, - 80, - 81, - 82, 85, 86, 88, @@ -2713,6 +2660,7 @@ "for_club": null, "name": "Chef de bus", "permissions": [ + 84, 117, 118, 120, @@ -2729,6 +2677,7 @@ "for_club": null, "name": "Chef d'\u00e9quipe", "permissions": [ + 84, 116, 123, 124, @@ -2742,7 +2691,9 @@ "fields": { "for_club": null, "name": "\u00c9lectron libre", - "permissions": [] + "permissions": [ + 84 + ] } }, { @@ -2751,7 +2702,9 @@ "fields": { "for_club": null, "name": "\u00c9lectron libre (avec perm)", - "permissions": [] + "permissions": [ + 84 + ] } }, { @@ -2771,7 +2724,6 @@ "name": "Adhérent WEI", "permissions": [ 77, - 84, 87, 90, 93, diff --git a/apps/permission/models.py b/apps/permission/models.py index beace783..d2ac2195 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -124,10 +124,10 @@ class PermissionMask(models.Model): class Permission(models.Model): PERMISSION_TYPES = [ - ('add', 'add'), - ('view', 'view'), - ('change', 'change'), - ('delete', 'delete') + ('add', _('add')), + ('view', _('view')), + ('change', _('change')), + ('delete', _('delete')) ] model = models.ForeignKey( diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index f7b8412f..6a8cb929 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-31 17:00+0200\n" +"POT-Creation-Date: 2020-07-31 22:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -46,7 +46,7 @@ msgstr "" #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/note/models/notes.py:188 #: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 -#: apps/note/models/transactions.py:261 apps/permission/models.py:323 +#: apps/note/models/transactions.py:263 apps/permission/models.py:332 #: apps/wei/models.py:65 apps/wei/models.py:117 #: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -71,14 +71,14 @@ msgid "activity types" msgstr "" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:104 apps/permission/models.py:183 +#: apps/permission/models.py:113 apps/permission/models.py:192 #: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:158 +#: apps/note/models/transactions.py:66 apps/permission/models.py:167 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "" @@ -193,7 +193,7 @@ msgstr "" #: apps/activity/tables.py:79 apps/member/forms.py:107 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 -#: templates/note/transaction_form.html:126 +#: templates/note/transaction_form.html:127 msgid "First name" msgstr "" @@ -241,7 +241,7 @@ msgstr "" msgid "IP Address" msgstr "" -#: apps/logs/models.py:35 apps/permission/models.py:128 +#: apps/logs/models.py:35 apps/permission/models.py:137 msgid "model" msgstr "" @@ -267,7 +267,7 @@ msgid "edit" msgstr "" #: apps/logs/models.py:62 apps/note/tables.py:138 apps/note/tables.py:166 -#: apps/wei/tables.py:65 +#: apps/permission/models.py:130 apps/wei/tables.py:65 msgid "delete" msgstr "" @@ -324,7 +324,7 @@ msgid "Credit amount" msgstr "" #: apps/member/forms.py:112 apps/registration/forms.py:74 -#: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:133 msgid "Bank" msgstr "" @@ -533,7 +533,7 @@ msgstr "" msgid "fee" msgstr "" -#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:787 +#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:797 msgid "User is not a member of the parent club" msgstr "" @@ -613,7 +613,7 @@ msgstr "" msgid "Add new member to the club" msgstr "" -#: apps/member/views.py:529 apps/wei/views.py:778 +#: apps/member/views.py:529 apps/wei/views.py:788 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -827,46 +827,46 @@ msgid "" "is not active." msgstr "" -#: apps/note/models/transactions.py:228 +#: apps/note/models/transactions.py:230 #: templates/activity/activity_entry.html:13 templates/base.html:99 #: templates/note/transaction_form.html:15 -#: templates/note/transaction_form.html:140 +#: templates/note/transaction_form.html:141 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:251 +#: apps/note/models/transactions.py:253 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:266 +#: apps/note/models/transactions.py:268 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:271 +#: apps/note/models/transactions.py:273 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:277 +#: apps/note/models/transactions.py:279 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:20 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:277 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:279 templates/note/transaction_form.html:24 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:288 +#: apps/note/models/transactions.py:290 msgid "" "A special transaction is only possible between a Note associated to a " "payment method and a User or a Club" msgstr "" -#: apps/note/models/transactions.py:305 apps/note/models/transactions.py:310 +#: apps/note/models/transactions.py:307 apps/note/models/transactions.py:312 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:306 apps/treasury/models.py:227 +#: apps/note/models/transactions.py:308 apps/treasury/models.py:227 msgid "membership transactions" msgstr "" @@ -915,67 +915,79 @@ msgstr "" msgid "Consumptions" msgstr "" -#: apps/permission/models.py:83 +#: apps/permission/models.py:92 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/permission/models.py:85 +#: apps/permission/models.py:94 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:98 +#: apps/permission/models.py:107 msgid "rank" msgstr "" -#: apps/permission/models.py:111 +#: apps/permission/models.py:120 msgid "permission mask" msgstr "" -#: apps/permission/models.py:112 +#: apps/permission/models.py:121 msgid "permission masks" msgstr "" -#: apps/permission/models.py:152 +#: apps/permission/models.py:127 +msgid "add" +msgstr "" + +#: apps/permission/models.py:128 +msgid "view" +msgstr "" + +#: apps/permission/models.py:129 +msgid "change" +msgstr "" + +#: apps/permission/models.py:161 msgid "query" msgstr "" -#: apps/permission/models.py:165 +#: apps/permission/models.py:174 msgid "mask" msgstr "" -#: apps/permission/models.py:171 +#: apps/permission/models.py:180 msgid "field" msgstr "" -#: apps/permission/models.py:176 +#: apps/permission/models.py:185 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." msgstr "" -#: apps/permission/models.py:177 templates/permission/all_rights.html:26 +#: apps/permission/models.py:186 templates/permission/all_rights.html:26 msgid "permanent" msgstr "" -#: apps/permission/models.py:188 +#: apps/permission/models.py:197 msgid "permission" msgstr "" -#: apps/permission/models.py:189 apps/permission/models.py:328 +#: apps/permission/models.py:198 apps/permission/models.py:337 msgid "permissions" msgstr "" -#: apps/permission/models.py:194 +#: apps/permission/models.py:203 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/permission/models.py:333 +#: apps/permission/models.py:342 msgid "for club" msgstr "" -#: apps/permission/models.py:343 apps/permission/models.py:344 +#: apps/permission/models.py:352 apps/permission/models.py:353 msgid "role permissions" msgstr "" @@ -1000,7 +1012,7 @@ msgid "" "{model_name}." msgstr "" -#: apps/permission/views.py:44 templates/base.html:135 +#: apps/permission/views.py:44 templates/base.html:136 msgid "Rights" msgstr "" @@ -1133,7 +1145,7 @@ msgstr "" msgid "Description" msgstr "" -#: apps/treasury/models.py:48 templates/note/transaction_form.html:120 +#: apps/treasury/models.py:48 templates/note/transaction_form.html:121 msgid "Name" msgstr "" @@ -1534,113 +1546,113 @@ msgstr "" msgid "members" msgstr "" -#: apps/wei/views.py:55 +#: apps/wei/views.py:56 msgid "Search WEI" msgstr "" -#: apps/wei/views.py:64 templates/wei/weiclub_list.html:9 +#: apps/wei/views.py:74 templates/wei/weiclub_list.html:10 msgid "Create WEI" msgstr "" -#: apps/wei/views.py:84 +#: apps/wei/views.py:94 msgid "WEI Detail" msgstr "" -#: apps/wei/views.py:179 +#: apps/wei/views.py:189 msgid "View members of the WEI" msgstr "" -#: apps/wei/views.py:207 +#: apps/wei/views.py:217 msgid "Find WEI Membership" msgstr "" -#: apps/wei/views.py:217 +#: apps/wei/views.py:227 msgid "View registrations to the WEI" msgstr "" -#: apps/wei/views.py:243 +#: apps/wei/views.py:253 msgid "Find WEI Registration" msgstr "" -#: apps/wei/views.py:254 +#: apps/wei/views.py:264 msgid "Update the WEI" msgstr "" -#: apps/wei/views.py:275 +#: apps/wei/views.py:285 msgid "Create new bus" msgstr "" -#: apps/wei/views.py:306 +#: apps/wei/views.py:316 msgid "Update bus" msgstr "" -#: apps/wei/views.py:336 +#: apps/wei/views.py:346 msgid "Manage bus" msgstr "" -#: apps/wei/views.py:363 +#: apps/wei/views.py:373 msgid "Create new team" msgstr "" -#: apps/wei/views.py:395 +#: apps/wei/views.py:405 msgid "Update team" msgstr "" -#: apps/wei/views.py:426 +#: apps/wei/views.py:436 msgid "Manage WEI team" msgstr "" -#: apps/wei/views.py:448 +#: apps/wei/views.py:458 msgid "Register first year student to the WEI" msgstr "" -#: apps/wei/views.py:460 templates/wei/weiclub_info.html:62 +#: apps/wei/views.py:470 templates/wei/weiclub_info.html:62 msgid "Register 1A" msgstr "" -#: apps/wei/views.py:481 apps/wei/views.py:551 +#: apps/wei/views.py:491 apps/wei/views.py:561 msgid "This user is already registered to this WEI." msgstr "" -#: apps/wei/views.py:486 +#: apps/wei/views.py:496 msgid "" "This user can't be in her/his first year since he/she has already participed " "to a WEI." msgstr "" -#: apps/wei/views.py:503 +#: apps/wei/views.py:513 msgid "Register old student to the WEI" msgstr "" -#: apps/wei/views.py:515 templates/wei/weiclub_info.html:65 +#: apps/wei/views.py:525 templates/wei/weiclub_info.html:65 msgid "Register 2A+" msgstr "" -#: apps/wei/views.py:533 apps/wei/views.py:621 +#: apps/wei/views.py:543 apps/wei/views.py:631 msgid "You already opened an account in the Société générale." msgstr "" -#: apps/wei/views.py:581 +#: apps/wei/views.py:591 msgid "Update WEI Registration" msgstr "" -#: apps/wei/views.py:671 +#: apps/wei/views.py:681 msgid "Delete WEI registration" msgstr "" -#: apps/wei/views.py:682 +#: apps/wei/views.py:692 msgid "You don't have the right to delete this WEI registration." msgstr "" -#: apps/wei/views.py:701 +#: apps/wei/views.py:711 msgid "Validate WEI registration" msgstr "" -#: apps/wei/views.py:782 +#: apps/wei/views.py:792 msgid "This user didn't give her/his caution check." msgstr "" -#: apps/wei/views.py:819 apps/wei/views.py:872 apps/wei/views.py:882 +#: apps/wei/views.py:829 apps/wei/views.py:882 apps/wei/views.py:892 #: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 #: templates/wei/survey_end.html:12 msgid "Survey WEI" @@ -1775,11 +1787,11 @@ msgstr "" msgid "Registrations" msgstr "" -#: templates/base.html:139 +#: templates/base.html:141 msgid "Administration" msgstr "" -#: templates/base.html:178 +#: templates/base.html:180 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." @@ -1950,7 +1962,7 @@ msgstr "" msgid "Double consumptions" msgstr "" -#: templates/note/conso_form.html:150 templates/note/transaction_form.html:151 +#: templates/note/conso_form.html:150 templates/note/transaction_form.html:152 msgid "Recent transactions history" msgstr "" @@ -1974,7 +1986,7 @@ msgstr "" msgid "Reason" msgstr "" -#: templates/note/transaction_form.html:110 +#: templates/note/transaction_form.html:111 msgid "Transfer type" msgstr "" @@ -2022,6 +2034,10 @@ msgstr "" msgid "Own this role in the clubs" msgstr "" +#: templates/permission/all_rights.html:26 +msgid "Mask:" +msgstr "" + #: templates/permission/all_rights.html:26 msgid "Query:" msgstr "" @@ -2366,7 +2382,7 @@ msgstr "" msgid "View WEI" msgstr "" -#: templates/wei/weiclub_list.html:16 +#: templates/wei/weiclub_list.html:18 msgid "WEI listing" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 91c6f1f7..6f12391c 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-07-31 17:00+0200\n" +"POT-Creation-Date: 2020-07-31 22:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,7 +47,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." #: apps/activity/models.py:23 apps/activity/models.py:48 #: apps/member/models.py:151 apps/note/models/notes.py:188 #: apps/note/models/transactions.py:25 apps/note/models/transactions.py:45 -#: apps/note/models/transactions.py:261 apps/permission/models.py:323 +#: apps/note/models/transactions.py:263 apps/permission/models.py:332 #: apps/wei/models.py:65 apps/wei/models.py:117 #: templates/member/club_info.html:13 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 @@ -72,14 +72,14 @@ msgid "activity types" msgstr "types d'activité" #: apps/activity/models.py:53 apps/note/models/transactions.py:81 -#: apps/permission/models.py:104 apps/permission/models.py:183 +#: apps/permission/models.py:113 apps/permission/models.py:192 #: apps/wei/models.py:71 apps/wei/models.py:128 #: templates/activity/activity_detail.html:16 msgid "description" msgstr "description" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:66 apps/permission/models.py:158 +#: apps/note/models/transactions.py:66 apps/permission/models.py:167 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" @@ -194,7 +194,7 @@ msgstr "Nom de famille" #: apps/activity/tables.py:79 apps/member/forms.py:107 #: apps/registration/forms.py:69 apps/treasury/forms.py:122 -#: templates/note/transaction_form.html:126 +#: templates/note/transaction_form.html:127 msgid "First name" msgstr "Prénom" @@ -242,7 +242,7 @@ msgstr "Logs" msgid "IP Address" msgstr "Adresse IP" -#: apps/logs/models.py:35 apps/permission/models.py:128 +#: apps/logs/models.py:35 apps/permission/models.py:137 msgid "model" msgstr "Modèle" @@ -268,7 +268,7 @@ msgid "edit" msgstr "Modifier" #: apps/logs/models.py:62 apps/note/tables.py:138 apps/note/tables.py:166 -#: apps/wei/tables.py:65 +#: apps/permission/models.py:130 apps/wei/tables.py:65 msgid "delete" msgstr "Supprimer" @@ -325,7 +325,7 @@ msgid "Credit amount" msgstr "Montant à créditer" #: apps/member/forms.py:112 apps/registration/forms.py:74 -#: apps/treasury/forms.py:124 templates/note/transaction_form.html:132 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:133 msgid "Bank" msgstr "Banque" @@ -538,7 +538,7 @@ msgstr "l'adhésion finit le" msgid "fee" msgstr "cotisation" -#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:787 +#: apps/member/models.py:303 apps/member/views.py:534 apps/wei/views.py:797 msgid "User is not a member of the parent club" msgstr "L'utilisateur n'est pas membre du club parent" @@ -618,7 +618,7 @@ msgstr "Modifier le club" msgid "Add new member to the club" msgstr "Ajouter un nouveau membre au club" -#: apps/member/views.py:529 apps/wei/views.py:778 +#: apps/member/views.py:529 apps/wei/views.py:788 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -837,36 +837,36 @@ msgstr "" "La transaction ne peut pas être sauvegardée puisque la note source ou la " "note de destination n'est pas active." -#: apps/note/models/transactions.py:228 +#: apps/note/models/transactions.py:230 #: templates/activity/activity_entry.html:13 templates/base.html:99 #: templates/note/transaction_form.html:15 -#: templates/note/transaction_form.html:140 +#: templates/note/transaction_form.html:141 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:251 +#: apps/note/models/transactions.py:253 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:266 +#: apps/note/models/transactions.py:268 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:271 +#: apps/note/models/transactions.py:273 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:277 +#: apps/note/models/transactions.py:279 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:20 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:277 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:279 templates/note/transaction_form.html:24 msgid "Debit" msgstr "Débit" -#: apps/note/models/transactions.py:288 +#: apps/note/models/transactions.py:290 msgid "" "A special transaction is only possible between a Note associated to a " "payment method and a User or a Club" @@ -874,11 +874,11 @@ msgstr "" "Une transaction spéciale n'est possible que entre une note associée à un " "mode de paiement et un utilisateur ou un club." -#: apps/note/models/transactions.py:305 apps/note/models/transactions.py:310 +#: apps/note/models/transactions.py:307 apps/note/models/transactions.py:312 msgid "membership transaction" msgstr "Transaction d'adhésion" -#: apps/note/models/transactions.py:306 apps/treasury/models.py:227 +#: apps/note/models/transactions.py:308 apps/treasury/models.py:227 msgid "membership transactions" msgstr "Transactions d'adhésion" @@ -927,41 +927,53 @@ msgstr "Modifier le bouton" msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:83 +#: apps/permission/models.py:92 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "Can {type} {model}.{field} in {query}" -#: apps/permission/models.py:85 +#: apps/permission/models.py:94 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "Can {type} {model} in {query}" -#: apps/permission/models.py:98 +#: apps/permission/models.py:107 msgid "rank" msgstr "Rang" -#: apps/permission/models.py:111 +#: apps/permission/models.py:120 msgid "permission mask" msgstr "masque de permissions" -#: apps/permission/models.py:112 +#: apps/permission/models.py:121 msgid "permission masks" msgstr "masques de permissions" -#: apps/permission/models.py:152 +#: apps/permission/models.py:127 +msgid "add" +msgstr "ajouter" + +#: apps/permission/models.py:128 +msgid "view" +msgstr "voir" + +#: apps/permission/models.py:129 +msgid "change" +msgstr "modifier" + +#: apps/permission/models.py:161 msgid "query" msgstr "requête" -#: apps/permission/models.py:165 +#: apps/permission/models.py:174 msgid "mask" msgstr "masque" -#: apps/permission/models.py:171 +#: apps/permission/models.py:180 msgid "field" msgstr "champ" -#: apps/permission/models.py:176 +#: apps/permission/models.py:185 msgid "" "Tells if the permission should be granted even if the membership of the user " "is expired." @@ -969,29 +981,29 @@ msgstr "" "Indique si la permission doit être attribuée même si l'adhésion de " "l'utilisateur est expirée." -#: apps/permission/models.py:177 templates/permission/all_rights.html:26 +#: apps/permission/models.py:186 templates/permission/all_rights.html:26 msgid "permanent" msgstr "permanent" -#: apps/permission/models.py:188 +#: apps/permission/models.py:197 msgid "permission" msgstr "permission" -#: apps/permission/models.py:189 apps/permission/models.py:328 +#: apps/permission/models.py:198 apps/permission/models.py:337 msgid "permissions" msgstr "permissions" -#: apps/permission/models.py:194 +#: apps/permission/models.py:203 msgid "Specifying field applies only to view and change permission types." msgstr "" "Spécifie le champ concerné, ne fonctionne que pour les permissions view et " "change." -#: apps/permission/models.py:333 +#: apps/permission/models.py:342 msgid "for club" msgstr "s'applique au club" -#: apps/permission/models.py:343 apps/permission/models.py:344 +#: apps/permission/models.py:352 apps/permission/models.py:353 msgid "role permissions" msgstr "Permissions par rôles" @@ -1022,7 +1034,7 @@ msgstr "" "Vous n'avez pas la permission de supprimer cette instance du modèle " "{app_label}.{model_name}." -#: apps/permission/views.py:44 templates/base.html:135 +#: apps/permission/views.py:44 templates/base.html:136 msgid "Rights" msgstr "Droits" @@ -1160,7 +1172,7 @@ msgstr "Objet" msgid "Description" msgstr "Description" -#: apps/treasury/models.py:48 templates/note/transaction_form.html:120 +#: apps/treasury/models.py:48 templates/note/transaction_form.html:121 msgid "Name" msgstr "Nom" @@ -1578,75 +1590,75 @@ msgstr "Nombre de membres" msgid "members" msgstr "adhérents" -#: apps/wei/views.py:55 +#: apps/wei/views.py:56 msgid "Search WEI" msgstr "Chercher un WEI" -#: apps/wei/views.py:64 templates/wei/weiclub_list.html:9 +#: apps/wei/views.py:74 templates/wei/weiclub_list.html:10 msgid "Create WEI" msgstr "Créer un WEI" -#: apps/wei/views.py:84 +#: apps/wei/views.py:94 msgid "WEI Detail" msgstr "Détails du WEI" -#: apps/wei/views.py:179 +#: apps/wei/views.py:189 msgid "View members of the WEI" msgstr "Voir les membres du WEI" -#: apps/wei/views.py:207 +#: apps/wei/views.py:217 msgid "Find WEI Membership" msgstr "Trouver une adhésion au WEI" -#: apps/wei/views.py:217 +#: apps/wei/views.py:227 msgid "View registrations to the WEI" msgstr "Voir les inscriptions au WEI" -#: apps/wei/views.py:243 +#: apps/wei/views.py:253 msgid "Find WEI Registration" msgstr "Trouver une inscription au WEI" -#: apps/wei/views.py:254 +#: apps/wei/views.py:264 msgid "Update the WEI" msgstr "Modifier le WEI" -#: apps/wei/views.py:275 +#: apps/wei/views.py:285 msgid "Create new bus" msgstr "Ajouter un nouveau bus" -#: apps/wei/views.py:306 +#: apps/wei/views.py:316 msgid "Update bus" msgstr "Modifier le bus" -#: apps/wei/views.py:336 +#: apps/wei/views.py:346 msgid "Manage bus" msgstr "Gérer le bus" -#: apps/wei/views.py:363 +#: apps/wei/views.py:373 msgid "Create new team" msgstr "Créer une nouvelle équipe" -#: apps/wei/views.py:395 +#: apps/wei/views.py:405 msgid "Update team" msgstr "Modifier l'équipe" -#: apps/wei/views.py:426 +#: apps/wei/views.py:436 msgid "Manage WEI team" msgstr "Gérer l'équipe WEI" -#: apps/wei/views.py:448 +#: apps/wei/views.py:458 msgid "Register first year student to the WEI" msgstr "Inscrire un 1A au WEI" -#: apps/wei/views.py:460 templates/wei/weiclub_info.html:62 +#: apps/wei/views.py:470 templates/wei/weiclub_info.html:62 msgid "Register 1A" msgstr "Inscrire un 1A" -#: apps/wei/views.py:481 apps/wei/views.py:551 +#: apps/wei/views.py:491 apps/wei/views.py:561 msgid "This user is already registered to this WEI." msgstr "Cette personne est déjà inscrite au WEI." -#: apps/wei/views.py:486 +#: apps/wei/views.py:496 msgid "" "This user can't be in her/his first year since he/she has already participed " "to a WEI." @@ -1654,39 +1666,39 @@ msgstr "" "Cet utilisateur ne peut pas être en première année puisqu'iel a déjà " "participé à un WEI." -#: apps/wei/views.py:503 +#: apps/wei/views.py:513 msgid "Register old student to the WEI" msgstr "Inscrire un 2A+ au WEI" -#: apps/wei/views.py:515 templates/wei/weiclub_info.html:65 +#: apps/wei/views.py:525 templates/wei/weiclub_info.html:65 msgid "Register 2A+" msgstr "Inscrire un 2A+" -#: apps/wei/views.py:533 apps/wei/views.py:621 +#: apps/wei/views.py:543 apps/wei/views.py:631 msgid "You already opened an account in the Société générale." msgstr "Vous avez déjà ouvert un compte auprès de la société générale." -#: apps/wei/views.py:581 +#: apps/wei/views.py:591 msgid "Update WEI Registration" msgstr "Modifier l'inscription WEI" -#: apps/wei/views.py:671 +#: apps/wei/views.py:681 msgid "Delete WEI registration" msgstr "Supprimer l'inscription WEI" -#: apps/wei/views.py:682 +#: apps/wei/views.py:692 msgid "You don't have the right to delete this WEI registration." msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI." -#: apps/wei/views.py:701 +#: apps/wei/views.py:711 msgid "Validate WEI registration" msgstr "Valider l'inscription WEI" -#: apps/wei/views.py:782 +#: apps/wei/views.py:792 msgid "This user didn't give her/his caution check." msgstr "Cet utilisateur n'a pas donné son chèque de caution." -#: apps/wei/views.py:819 apps/wei/views.py:872 apps/wei/views.py:882 +#: apps/wei/views.py:829 apps/wei/views.py:882 apps/wei/views.py:892 #: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 #: templates/wei/survey_end.html:12 msgid "Survey WEI" @@ -1830,11 +1842,11 @@ msgstr "Clubs" msgid "Registrations" msgstr "Inscriptions" -#: templates/base.html:139 +#: templates/base.html:141 msgid "Administration" msgstr "Administration" -#: templates/base.html:178 +#: templates/base.html:180 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." @@ -2010,7 +2022,7 @@ msgstr "Consommations simples" msgid "Double consumptions" msgstr "Consommations doubles" -#: templates/note/conso_form.html:150 templates/note/transaction_form.html:151 +#: templates/note/conso_form.html:150 templates/note/transaction_form.html:152 msgid "Recent transactions history" msgstr "Historique des transactions récentes" @@ -2034,7 +2046,7 @@ msgstr "Action" msgid "Reason" msgstr "Raison" -#: templates/note/transaction_form.html:110 +#: templates/note/transaction_form.html:111 msgid "Transfer type" msgstr "Type de transfert" @@ -2082,6 +2094,10 @@ msgstr "Filtrer les rôles que je possède dans au moins un club" msgid "Own this role in the clubs" msgstr "Possède ce rôle dans les clubs" +#: templates/permission/all_rights.html:26 +msgid "Mask:" +msgstr "Masque :" + #: templates/permission/all_rights.html:26 msgid "Query:" msgstr "Requête :" @@ -2470,7 +2486,7 @@ msgstr "Ajouter un bus" msgid "View WEI" msgstr "Voir le WEI" -#: templates/wei/weiclub_list.html:16 +#: templates/wei/weiclub_list.html:18 msgid "WEI listing" msgstr "Liste des WEI" diff --git a/templates/base.html b/templates/base.html index 744a8714..2d940e2b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -131,9 +131,11 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'WEI' %}
  • {% endif %} - + {% if user.is_authenticated %} + + {% endif %} {% if user.is_staff and ""|has_perm:user %} {% endif %} - {% if "member.change_profile_registration_valid"|has_perm:user %} - - {% endif %} {% if "activity.activity"|not_empty_model_list %} {% endif %} diff --git a/templates/member/user_list.html b/templates/member/user_list.html index bad37468..658292b0 100644 --- a/templates/member/user_list.html +++ b/templates/member/user_list.html @@ -2,6 +2,7 @@ {% load render_table from django_tables2 %} {% load crispy_forms_tags %} {% load i18n %} +{% load perms %} {% block content %} @@ -18,6 +19,13 @@ {% endif %} +
    + + {% if "member.change_profile_registration_valid"|has_perm:user %} + + {% trans "Registrations" %} + + {% endif %} {% endblock %} {% block extrajavascript %} From 377397b3190ecefa91d21d43190e907bf11fae47 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 1 Aug 2020 15:59:39 +0200 Subject: [PATCH 76/77] :bug: Fix WEI tables --- apps/note/admin.py | 1 - apps/wei/tables.py | 2 +- templates/wei/weilist_sample.tex | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/note/admin.py b/apps/note/admin.py index 2d140daf..433ef2dc 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -38,7 +38,6 @@ class NoteAdmin(PolymorphicParentModelAdmin): # Organize notes by registration date date_hierarchy = 'created_at' - ordering = ['name'] # Search by aliases search_fields = ['alias__name'] diff --git a/apps/wei/tables.py b/apps/wei/tables.py index a6bc25c6..41c35a47 100644 --- a/apps/wei/tables.py +++ b/apps/wei/tables.py @@ -103,7 +103,7 @@ class WEIMembershipTable(tables.Table): team = tables.LinkColumn( 'wei:manage_bus_team', - args=[A('bus.pk')], + args=[A('team.pk')], ) def render_year(self, record): diff --git a/templates/wei/weilist_sample.tex b/templates/wei/weilist_sample.tex index 19eb0cdc..09d3f6be 100644 --- a/templates/wei/weilist_sample.tex +++ b/templates/wei/weilist_sample.tex @@ -14,11 +14,11 @@ \huge{Liste des inscrits \og {{ wei.name }} \fg{}} {% if bus %} -\LARGE{Bus {{ bus.name }}} +\LARGE{Bus {{ bus.name|safe }}} {% if team %} -\Large{Équipe {{ team.name }}} +\Large{Équipe {{ team.name|safe }}} {% endif %} {% endif %} \end{center} From b5586c647bf41f87ab2c9b4039a58278074706c4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Sat, 1 Aug 2020 16:07:47 +0200 Subject: [PATCH 77/77] :green_heart: Fix linters --- apps/member/admin.py | 1 - apps/member/forms.py | 17 +++++++++-------- apps/member/views.py | 8 ++++---- apps/treasury/admin.py | 1 - 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/member/admin.py b/apps/member/admin.py index 26695b3b..bd29557b 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -5,7 +5,6 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ - from note.templatetags.pretty_money import pretty_money from note_kfet.admin import admin_site diff --git a/apps/member/forms.py b/apps/member/forms.py index c4849e2a..50fa9c47 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -132,19 +132,20 @@ class MembershipForm(forms.ModelForm): 'date_start': DatePickerInput(), } + class MembershipRolesForm(forms.ModelForm): user = forms.ModelChoiceField( queryset=User.objects, label=_("User"), disabled=True, widget=Autocomplete( - User, - attrs={ - 'api_url': '/api/user/', - 'name_field': 'username', - 'placeholder': 'Nom ...', - }, - ), + User, + attrs={ + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', + }, + ), ) roles = forms.ModelMultipleChoiceField( @@ -154,4 +155,4 @@ class MembershipRolesForm(forms.ModelForm): class Meta: model = Membership - fields = ('user', 'roles') \ No newline at end of file + fields = ('user', 'roles') diff --git a/apps/member/views.py b/apps/member/views.py index 2826d11b..30fbb139 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -672,9 +672,9 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV if 'search' in self.request.GET: pattern = self.request.GET['search'] qs = qs.filter( - Q(user__first_name__iregex='^' + pattern) | - Q(user__last_name__iregex='^' + pattern) | - Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern)) + Q(user__first_name__iregex='^' + pattern) + | Q(user__last_name__iregex='^' + pattern) + | Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern)) ) only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' @@ -706,4 +706,4 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0' - return context \ No newline at end of file + return context diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py index 6e2d4304..33224ba7 100644 --- a/apps/treasury/admin.py +++ b/apps/treasury/admin.py @@ -39,4 +39,3 @@ class SogeCreditAdmin(admin.ModelAdmin): def has_add_permission(self, request): # Don't create a credit manually return False -