@@ -44,6 +44,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
@@ -27,7 +24,7 @@
Ensuite, un rapport vous est envoyé à la fréquence demandée seulement si vous avez consommé
depuis le dernier rapport.
Pour arrêter de recevoir des rapports, il vous suffit de modifier votre profil Note et de
- mettre la fréquence des rapports à 0 ou -1.
+ mettre la fréquence des rapports à 0.
Pour toutes suggestions par rapport à ce service, contactez
notekfet2020@lists.crans.org.
@@ -65,9 +65,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
@@ -97,8 +97,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% trans "Action" %} @@ -153,7 +153,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
{% trans "Recent transactions history" %} @@ -176,5 +176,5 @@ SPDX-License-Identifier: GPL-2.0-or-later select_receveirs_label = "{% trans "Select receivers" %}"; transfer_type_label = "{% trans "Transfer type" %}"; - + {% endblock %} diff --git a/apps/note/views.py b/apps/note/views.py index f5e18290..50fcd78d 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -144,7 +144,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ The Magic View that make people pay their beer and burgers. - (Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`) + (Most of the magic happens in the dark world of Javascript see `static/note/js/consos.js`) """ model = Transaction template_name = "note/conso_form.html" diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 120bb1ee..a3db88e8 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -1103,7 +1103,7 @@ "treasury", "sogecredit" ], - "query": "{\"credit_transaction\": null}", + "query": "{}", "type": "add", "mask": 1, "field": "", @@ -2567,6 +2567,182 @@ "description": "(Dé)bloquer sa propre note et modifier la raison" } }, + { + "model": "permission.permission", + "pk": 165, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{}", + "type": "change", + "mask": 1, + "field": "password", + "permanent": true, + "description": "Changer son mot de passe" + } + }, + { + "model": "permission.permission", + "pk": 166, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]", + "type": "add", + "mask": 2, + "field": "", + "permanent": false, + "description": "Créer une transaction quelconque tant que la source reste au-dessus de -50 €" + } + }, + { + "model": "permission.permission", + "pk": 167, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]", + "type": "change", + "mask": 2, + "field": "valid", + "permanent": false, + "description": "Modifier le statut de validation d'une transaction si c'est possible" + } + }, + { + "model": "permission.permission", + "pk": 168, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]", + "type": "change", + "mask": 2, + "field": "invalidity_reason", + "permanent": false, + "description": "Modifier la raison d'invalidité d'une transaction si c'est possible" + } + }, + { + "model": "permission.permission", + "pk": 169, + "fields": { + "model": [ + "note", + "noteclub" + ], + "query": "{\"club\": [\"club\"]}", + "type": "change", + "mask": 1, + "field": "display_image", + "permanent": false, + "description": "Changer l'image de la note de son club" + } + }, + { + "model": "permission.permission", + "pk": 170, + "fields": { + "model": [ + "note", + "alias" + ], + "query": "{\"note__is_active\": true}", + "type": "add", + "mask": 1, + "field": "", + "permanent": false, + "description": "Ajouter n'importe quel alias à une note non bloquée" + } + }, + { + "model": "permission.permission", + "pk": 171, + "fields": { + "model": [ + "note", + "alias" + ], + "query": "{\"note__is_active\": true}", + "type": "delete", + "mask": 3, + "field": "", + "permanent": false, + "description": "Supprimer n'importe quel alias à une note non bloquée" + } + }, + { + "model": "permission.permission", + "pk": 172, + "fields": { + "model": [ + "treasury", + "remittance" + ], + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "permanent": false, + "description": "Voir toutes les remises" + } + }, + { + "model": "permission.permission", + "pk": 173, + "fields": { + "model": [ + "treasury", + "remittance" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "permanent": false, + "description": "Ajouter une remise" + } + }, + { + "model": "permission.permission", + "pk": 174, + "fields": { + "model": [ + "treasury", + "remittance" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "permanent": false, + "description": "Modifier une remise" + } + }, + { + "model": "permission.permission", + "pk": 175, + "fields": { + "model": [ + "treasury", + "remittance" + ], + "query": "{}", + "type": "delete", + "mask": 3, + "field": "", + "permanent": false, + "description": "Supprimer une remise" + } + }, { "model": "permission.role", "pk": 1, @@ -2591,7 +2767,8 @@ 52, 126, 161, - 162 + 162, + 165 ] } }, @@ -2661,7 +2838,8 @@ 47, 49, 50, - 141 + 141, + 169 ] } }, @@ -2696,8 +2874,14 @@ 62, 127, 133, + 135, + 136, 141, - 142 + 142, + 150, + 166, + 167, + 168 ] } }, @@ -2711,8 +2895,7 @@ 24, 25, 26, - 27, - 33 + 27 ] } }, @@ -2763,7 +2946,13 @@ 150, 151, 163, - 164 + 164, + 170, + 171, + 172, + 173, + 174, + 175 ] } }, @@ -2932,7 +3121,18 @@ 161, 162, 163, - 164 + 164, + 165, + 166, + 167, + 168, + 169, + 170, + 171, + 172, + 173, + 174, + 175 ] } }, @@ -2944,7 +3144,6 @@ "name": "GC Kfet", "permissions": [ 32, - 33, 56, 58, 55, @@ -2959,7 +3158,13 @@ 29, 30, 31, - 143 + 70, + 143, + 166, + 167, + 168, + 170, + 171 ] } }, diff --git a/apps/permission/migrations/0001_initial.py b/apps/permission/migrations/0001_initial.py new file mode 100644 index 00000000..c82aa032 --- /dev/null +++ b/apps/permission/migrations/0001_initial.py @@ -0,0 +1,71 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('member', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Permission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('query', models.TextField(verbose_name='query')), + ('type', models.CharField(choices=[('add', 'add'), ('view', 'view'), ('change', 'change'), ('delete', 'delete')], max_length=15, verbose_name='type')), + ('field', models.CharField(blank=True, max_length=255, verbose_name='field')), + ('permanent', models.BooleanField(default=False, help_text='Tells if the permission should be granted even if the membership of the user is expired.', verbose_name='permanent')), + ('description', models.CharField(blank=True, max_length=255, verbose_name='description')), + ], + options={ + 'verbose_name': 'permission', + 'verbose_name_plural': 'permissions', + }, + ), + migrations.CreateModel( + name='PermissionMask', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rank', models.PositiveSmallIntegerField(unique=True, verbose_name='rank')), + ('description', models.CharField(max_length=255, unique=True, verbose_name='description')), + ], + options={ + 'verbose_name': 'permission mask', + 'verbose_name_plural': 'permission masks', + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('for_club', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club')), + ('permissions', models.ManyToManyField(to='permission.Permission', verbose_name='permissions')), + ], + options={ + 'verbose_name': 'role permissions', + 'verbose_name_plural': 'role permissions', + }, + ), + migrations.AddField( + model_name='permission', + name='mask', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='permissions', to='permission.PermissionMask', verbose_name='mask'), + ), + migrations.AddField( + model_name='permission', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.ContentType', verbose_name='model'), + ), + migrations.AlterUniqueTogether( + name='permission', + unique_together={('model', 'query', 'type', 'field')}, + ), + ] diff --git a/apps/permission/models.py b/apps/permission/models.py index 3d395a75..ff348404 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -57,8 +57,8 @@ class InstancedPermission: # Force insertion, no data verification, no trigger obj._force_save = True - # We don't want log anything - obj._no_log = True + # We don't want to trigger any signal (log, ...) + obj._no_signal = True Model.save(obj, force_insert=True) ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists() # Delete testing object diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index 03f07992..f2ca28f0 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -14,6 +14,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): This is a simple patch of this class that controls view access. """ + # The queryset is filtered, and permissions are more powerful than a simple check than just "can view this model" perms_map = { 'GET': ['%(app_label)s.view_%(model_name)s'], 'OPTIONS': [], diff --git a/apps/permission/signals.py b/apps/permission/signals.py index 112247eb..e738545a 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -28,7 +28,7 @@ def pre_save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return - if hasattr(instance, "_force_save"): + if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"): return user = get_current_authenticated_user() @@ -82,7 +82,8 @@ def pre_delete_object(instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return - if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0: + if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0 \ + or hasattr(instance, "_no_signal"): # Don't check permissions on force-deleted objects return diff --git a/apps/permission/tests/test_permission_queries.py b/apps/permission/tests/test_permission_queries.py index e0af9cf0..fdd530a5 100644 --- a/apps/permission/tests/test_permission_queries.py +++ b/apps/permission/tests/test_permission_queries.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from datetime import date +from json.decoder import JSONDecodeError from django.contrib.auth.models import User from django.core.exceptions import FieldError @@ -56,29 +57,28 @@ class PermissionQueryTestCase(TestCase): We use a random user with a random WEIClub (to use permissions for the WEI) in a random team in a random bus. """ for perm in Permission.objects.all(): - instanced = perm.about( - user=User.objects.get(), - club=WEIClub.objects.get(), - membership=Membership.objects.get(), - User=User, - Club=Club, - Membership=Membership, - Note=Note, - NoteUser=NoteUser, - NoteClub=NoteClub, - NoteSpecial=NoteSpecial, - F=F, - Q=Q, - now=timezone.now(), - today=date.today(), - ) try: + instanced = perm.about( + user=User.objects.get(), + club=WEIClub.objects.get(), + membership=Membership.objects.get(), + User=User, + Club=Club, + Membership=Membership, + Note=Note, + NoteUser=NoteUser, + NoteClub=NoteClub, + NoteSpecial=NoteSpecial, + F=F, + Q=Q, + now=timezone.now(), + today=date.today(), + ) instanced.update_query() query = instanced.query model = perm.model.model_class() model.objects.filter(query).all() - # print("Good query for permission", perm) - except (FieldError, AttributeError, ValueError, TypeError): + except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): print("Query error for permission", perm) print("Query:", perm.query) if instanced.query: diff --git a/apps/permission/tests/test_rights_page.py b/apps/permission/tests/test_rights_page.py new file mode 100644 index 00000000..da80bf09 --- /dev/null +++ b/apps/permission/tests/test_rights_page.py @@ -0,0 +1,44 @@ +# 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.test import TestCase +from django.urls import reverse +from member.models import Membership, Club +from permission.models import Role + + +class TestRightsPage(TestCase): + """ + Display the rights page. + """ + fixtures = ("initial",) + + def test_anonymous_rights_page(self): + """ + Check that we can properly see the rights page even if we are not connected. + We can't nethertheless see the club managers. + """ + response = self.client.get(reverse("permission:rights")) + self.assertEqual(response.status_code, 200) + self.assertFalse("special_memberships_table" in response.context) + self.assertFalse("superusers" in response.context) + + def test_authenticated_rights_page(self): + """ + Connect to the note and check that the club mangers are also displayed. + """ + user = User.objects.create_superuser( + username="ploptoto", + password="totototo", + email="toto@example.com", + ) + self.client.force_login(user) + membership = Membership.objects.create(user=user, club=Club.objects.get(name="BDE")) + membership.roles.add(Role.objects.get(name="Respo info")) + membership.save() + + response = self.client.get(reverse("permission:rights")) + self.assertEqual(response.status_code, 200) + self.assertIsNotNone(response.context["special_memberships_table"]) + self.assertIsNotNone(response.context["superusers"]) diff --git a/apps/permission/views.py b/apps/permission/views.py index 70aa7184..343152f5 100644 --- a/apps/permission/views.py +++ b/apps/permission/views.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.db.models import Q from django.forms import HiddenInput +from django.http import Http404 from django.utils.translation import gettext_lazy as _ from django.views.generic import UpdateView, TemplateView, CreateView from member.models import Membership @@ -24,9 +25,20 @@ class ProtectQuerysetMixin: Display 404 error if the user can't see an object, remove the fields the user can't update on an update form (useful if the user can't change only specified fields). """ - def get_queryset(self, **kwargs): + def get_queryset(self, filter_permissions=True, **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")).distinct()\ + if filter_permissions else qs + + def get_object(self, queryset=None): + try: + return super().get_object(queryset) + except Http404 as e: + try: + super().get_object(self.get_queryset(filter_permissions=False)) + raise PermissionDenied() + except Http404: + raise e def get_form(self, form_class=None): form = super().get_form(form_class) diff --git a/apps/registration/static/registration/css/login.css b/apps/registration/static/registration/css/login.css index b789b2f7..b1bac970 100644 --- a/apps/registration/static/registration/css/login.css +++ b/apps/registration/static/registration/css/login.css @@ -16,4 +16,8 @@ Font-Awesome attribution is already done inside SVG files #login-form select { -moz-appearance: none; cursor: pointer; -} \ No newline at end of file +} + +#login-form .asteriskField { + display: none; +} diff --git a/apps/registration/templates/registration/email_validation_complete.html b/apps/registration/templates/registration/email_validation_complete.html index dca26470..11a84fe9 100644 --- a/apps/registration/templates/registration/email_validation_complete.html +++ b/apps/registration/templates/registration/email_validation_complete.html @@ -5,14 +5,29 @@ SPDX-License-Identifier: GPL-3.0-or-later {% load i18n %} {% block content %} - {% if validlink %} - {% trans "Your email have successfully been validated." %} +
+ {% trans "Your email have successfully been validated." %} +
{% if user_object.profile.registration_valid %} +{% blocktrans %}You can now log in.{% endblocktrans %} +
{% else %} +{% trans "You must pay now your membership in the Kfet to complete your registration." %} +
{% endif %} - {% else %} - {% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %} - {% endif %} + {% else %} ++ {% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %} +
+ {% endif %} +- {% trans "An email has been sent. Please click on the link to activate your account." %} -
- -- {% trans "You must also go to the Kfet to pay your membership. The WEI registration includes the BDE membership." %} -
-{% endblock %} \ No newline at end of file ++ {% trans "An email has been sent. Please click on the link to activate your account." %} +
++ {% trans "You must also go to the Kfet to pay your membership." %} +
+- {% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet. Note that the WEI registration includes the Kfet membership." %} + {% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %}
@@ -38,4 +38,4 @@
{% trans "The Note Kfet team." %}
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
-
Default content...
@@ -178,7 +169,7 @@ SPDX-License-Identifier: GPL-3.0-or-later class="form-inline"> Nous contacter — + class="text-muted">{% trans "Contact us" %} — {% csrf_token %}