mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-26 05:23:18 +01:00 
			
		
		
		
	Compare commits
	
		
			199 Commits
		
	
	
		
			nix-shell
			...
			3f09d991f7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3f09d991f7 | ||
|  | abc4f14bd1 | ||
|  | 47138bafd4 | ||
|  | a3920fcae3 | ||
|  | ae4213d087 | ||
|  | cbf92651f0 | ||
|  | 12c93ff9da | ||
|  | 354c79bb82 | ||
|  | 35ffbfcf55 | ||
|  | 581715d804 | ||
| d595d908c6 | |||
| 734f5b242d | |||
| b0c7d43a50 | |||
|  | b8f81048a5 | ||
|  | af819f45a1 | ||
|  | 076d065ffa | ||
|  | 2da77d9c17 | ||
|  | 01584d6330 | ||
|  | 4c0a5922c4 | ||
|  | f90b28fc7c | ||
|  | 925e0f26f5 | ||
|  | c912383f86 | ||
|  | 32830e43fd | ||
|  | 11c6a6fa7a | ||
|  | 201d6b114a | ||
|  | 19e77df299 | ||
|  | 5fd6ec5668 | ||
|  | 10a01c5bc2 | ||
|  | 989905ea64 | ||
|  | 0218d43a17 | ||
|  | 5d30b0e819 | ||
|  | ec759dd3c0 | ||
|  | 2eb965291d | ||
|  | 7f182ee2ee | ||
|  | 3132aa4c38 | ||
|  | c7eb774859 | ||
|  | 32f8d285b3 | ||
|  | 050256ea13 | ||
|  | 7afd15b1cc | ||
|  | 258361f116 | ||
|  | a307530579 | ||
|  | 5de930bf40 | ||
|  | f7ebe0e99b | ||
|  | 73de6e2176 | ||
|  | 201611b105 | ||
|  | 40c239e9da | ||
|  | 2aaab2b454 | ||
|  | fc088dec86 | ||
|  | 2d60f1fd7b | ||
|  | 7b48b09329 | ||
|  | ffac940511 | ||
|  | 50f98fd5ad | ||
|  | 402e19d1ce | ||
|  | 0b0394b61f | ||
|  | 98422d8259 | ||
|  | 29509b5b26 | ||
|  | 0d64ad31e0 | ||
|  | 5781cbd6a5 | ||
|  | 5295e61a00 | ||
|  | e79ed6226a | ||
|  | 68152e6354 | ||
|  | 6c61daf1c5 | ||
|  | b8cc297baf | ||
|  | cd8224f2e0 | ||
|  | 3c882a7854 | ||
|  | 357e1bbaa2 | ||
|  | f5c4c58525 | ||
|  | dafb602b08 | ||
|  | 5b377e6a75 | ||
|  | 28bd62531e | ||
|  | b3a31c27a5 | ||
|  | c7a8e6a1a5 | ||
|  | 546a3a72b1 | ||
|  | 2e5664f79d | ||
|  | e367666fe9 | ||
|  | 04a9b3daf0 | ||
|  | d1df8f3eac | ||
|  | a5221f66ef | ||
|  | 7d59cd6cd2 | ||
|  | 0db0474217 | ||
|  | 2b3eb15f59 | ||
|  | a6b479db19 | ||
|  | 048d251f75 | ||
|  | 7b11cb0797 | ||
|  | ff3c30517e | ||
|  | f481ea6acb | ||
|  | 802fd8c2d7 | ||
|  | 5209a586a9 | ||
|  | 24f54ac876 | ||
|  | 988b4c9e88 | ||
|  | e32c267995 | ||
|  | 5e39209ab1 | ||
|  | 08b2fabe07 | ||
|  | 405479e5ad | ||
|  | 0cc130092f | ||
|  | ff6e207512 | ||
|  | 0f1e4d2e60 | ||
|  | 6255bcbbb1 | ||
|  | d82a1001c4 | ||
|  | 31a54482f0 | ||
|  | 4ee02345d4 | ||
|  | 422c087d17 | ||
|  | 30d6e2c95e | ||
|  | f3a3f07e38 | ||
|  | a5e802f370 | ||
|  | 540f3bc354 | ||
|  | 2d19457506 | ||
|  | 72786d0d2b | ||
|  | f099cbc879 | ||
|  | 977eb7c0d4 | ||
|  | d81b1f2710 | ||
|  | 6a69590a82 | ||
|  | 7afc583282 | ||
|  | 4fb0b7d736 | ||
|  | 18a5b65a1c | ||
|  | f545af4977 | ||
|  | 103e2d0635 | ||
|  | aedf0e87ba | ||
|  | dab45b5fd4 | ||
|  | b3353b563c | ||
|  | 6bc52be707 | ||
|  | 834d68fe35 | ||
|  | c6a2849d35 | ||
|  | 4ab22c92b3 | ||
|  | c328c1457c | ||
|  | 96da7d01ae | ||
|  | d27f942339 | ||
|  | 738d6c932d | ||
|  | 1760196578 | ||
|  | 13b9b6edea | ||
|  | e06e3b2972 | ||
|  | 9596aa7b8c | ||
|  | ba0d64f0d4 | ||
|  | 8d17801e28 | ||
|  | 609362c4f8 | ||
|  | 03d2d5f03e | ||
|  | d2057a9f45 | ||
|  | b6e68eeebe | ||
|  | 6410542027 | ||
|  | 6b1cd3ba7a | ||
|  | 9f114b8ca2 | ||
|  | e0132b6dc8 | ||
|  | f1cc82fab3 | ||
|  | 644cf14c4b | ||
|  | f19a489313 | ||
|  | dedd6c69cc | ||
|  | b42f5afeab | ||
|  | 31e67ae3f6 | ||
|  | b08da7a727 | ||
|  | 451aa64f33 | ||
|  | 3c99b0f3e9 | ||
|  | 201a179947 | ||
|  | 96784aee3b | ||
|  | 981c4d0300 | ||
|  | 11223430fd | ||
|  | 7aeb977e72 | ||
|  | 52fef1df42 | ||
|  | 16f8a60a3f | ||
|  | 2839d3de1e | ||
|  | 30afa6da0a | ||
|  | 84fc77696f | ||
|  | 19fc620d1f | ||
|  | d5819ac562 | ||
|  | a79df8f1f6 | ||
|  | 364b18e188 | ||
|  | 10a883b2e5 | ||
|  | 1410ab6c4f | ||
|  | 623dd61be6 | ||
|  | 48a0a87e7c | ||
|  | 563f525b11 | ||
|  | 63c1d74f1a | ||
|  | c42fb380a6 | ||
|  | c636d52a73 | ||
|  | 6a9021ec14 | ||
|  | 9c9149b53a | ||
|  | cb74311e7b | ||
|  | 9d7dd566c9 | ||
|  | 6bceb394c5 | ||
|  | 62cf8f9d84 | ||
|  | 9944ebcaad | ||
|  | 8537f043f7 | ||
|  | 2dd1c3fb89 | ||
|  | c8665c5798 | ||
|  | e9f1b6f52d | ||
|  | 1d95ae4810 | ||
|  | c89a95f8d2 | ||
|  | 73640b1dfa | ||
|  | 84b16ab603 | ||
|  | 6a1b51dbbf | ||
|  | c441a43a8b | ||
|  | 87f3b51b04 | ||
|  | 0a853fd3e6 | ||
|  | c429734810 | ||
|  | 5d759111b6 | ||
|  | 70baf7566c | ||
|  | eb355f547c | ||
|  | 7068170f18 | ||
|  | 45ee9a8941 | ||
|  | 454ea19603 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -42,11 +42,13 @@ map.json | ||||
| backups/ | ||||
| /static/ | ||||
| /media/ | ||||
| /tmp/ | ||||
|  | ||||
| # Virtualenv | ||||
| env/ | ||||
| venv/ | ||||
| db.sqlite3 | ||||
| shell.nix | ||||
|  | ||||
| # ansibles customs host | ||||
| ansible/host_vars/*.yaml | ||||
|   | ||||
| @@ -8,19 +8,19 @@ variables: | ||||
|   GIT_SUBMODULE_STRATEGY: recursive | ||||
|  | ||||
| # Debian Buster | ||||
| py37-django22: | ||||
|   stage: test | ||||
|   image: debian:buster-backports | ||||
|   before_script: | ||||
|     - > | ||||
|         apt-get update && | ||||
|         apt-get install --no-install-recommends -t buster-backports -y | ||||
|         python3-django python3-django-crispy-forms | ||||
|         python3-django-extensions python3-django-filters python3-django-polymorphic | ||||
|         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil | ||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache | ||||
|         python3-bs4 python3-setuptools tox texlive-xetex | ||||
|   script: tox -e py37-django22 | ||||
| #  py37-django22: | ||||
| #   stage: test | ||||
| #   image: debian:buster-backports | ||||
| #   before_script: | ||||
| #     - > | ||||
| #         apt-get update && | ||||
| #         apt-get install --no-install-recommends -t buster-backports -y | ||||
| #         python3-django python3-django-crispy-forms | ||||
| #         python3-django-extensions python3-django-filters python3-django-polymorphic | ||||
| #         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil | ||||
| #         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache | ||||
| #         python3-bs4 python3-setuptools tox texlive-xetex | ||||
| #   script: tox -e py37-django22 | ||||
|  | ||||
| # Ubuntu 20.04 | ||||
| py38-django22: | ||||
| @@ -56,7 +56,7 @@ py39-django22: | ||||
|  | ||||
| linters: | ||||
|   stage: quality-assurance | ||||
|   image: debian:buster-backports | ||||
|   image: debian:bullseye | ||||
|   before_script: | ||||
|     - apt-get update && apt-get install -y tox | ||||
|   script: tox -e linters | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,3 @@ | ||||
| [submodule "apps/scripts"] | ||||
| 	path = apps/scripts | ||||
| 	url = https://gitlab.crans.org/bde/nk20-scripts.git | ||||
| 	url = https://gitlab.crans.org/bde/nk20-scripts | ||||
|   | ||||
| @@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions, | ||||
|     (env)$ ./manage.py makemigrations | ||||
|     (env)$ ./manage.py migrate | ||||
|     (env)$ ./manage.py loaddata initial | ||||
|     (env)$ ./manage.py createsuperuser  # Création d'un utilisateur initial | ||||
|     (env)$ ./manage.py createsuperuser  # Création d'un⋅e utilisateur⋅rice initial | ||||
|     ``` | ||||
|  | ||||
| 6.  Enjoy : | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'activity.apps.ActivityConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib import admin | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|             "name": "Pot", | ||||
|             "manage_entries": true, | ||||
|             "can_invite": true, | ||||
|             "guest_entry_fee": 500 | ||||
|             "guest_entry_fee": 1000 | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -28,5 +28,25 @@ | ||||
|             "can_invite": false, | ||||
|             "guest_entry_fee": 0 | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "model": "activity.activitytype", | ||||
|         "pk": 5, | ||||
|         "fields": { | ||||
|             "name": "Soir\u00e9e avec entrées", | ||||
|             "manage_entries": true, | ||||
|             "can_invite": false, | ||||
|             "guest_entry_fee": 0 | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "model": "activity.activitytype", | ||||
|         "pk": 7, | ||||
|         "fields": { | ||||
|             "name": "Soir\u00e9e avec invitations", | ||||
|             "manage_entries": true, | ||||
|             "can_invite": true, | ||||
|             "guest_entry_fee": 0 | ||||
|         } | ||||
|     } | ||||
| ] | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import timedelta | ||||
|   | ||||
							
								
								
									
										18
									
								
								apps/activity/migrations/0003_auto_20240323_1422.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/activity/migrations/0003_auto_20240323_1422.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.2.28 on 2024-03-23 13:22 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('activity', '0002_auto_20200904_2341'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='activity', | ||||
|             name='description', | ||||
|             field=models.TextField(blank=True, default='', verbose_name='description'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import os | ||||
| @@ -66,6 +66,8 @@ class Activity(models.Model): | ||||
|  | ||||
|     description = models.TextField( | ||||
|         verbose_name=_('description'), | ||||
|         blank=True, | ||||
|         default="", | ||||
|     ) | ||||
|  | ||||
|     location = models.CharField( | ||||
| @@ -123,6 +125,14 @@ class Activity(models.Model): | ||||
|         verbose_name=_('open'), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("activity") | ||||
|         verbose_name_plural = _("activities") | ||||
|         unique_together = ("name", "date_start", "date_end",) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
| @@ -144,14 +154,6 @@ class Activity(models.Model): | ||||
|                 if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities() | ||||
|         return ret | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("activity") | ||||
|         verbose_name_plural = _("activities") | ||||
|         unique_together = ("name", "date_start", "date_end",) | ||||
|  | ||||
|  | ||||
| class Entry(models.Model): | ||||
|     """ | ||||
| @@ -252,14 +254,13 @@ class Guest(models.Model): | ||||
|         verbose_name=_("inviter"), | ||||
|     ) | ||||
|  | ||||
|     @property | ||||
|     def has_entry(self): | ||||
|         try: | ||||
|             if self.entry: | ||||
|                 return True | ||||
|             return False | ||||
|         except AttributeError: | ||||
|             return False | ||||
|     class Meta: | ||||
|         verbose_name = _("guest") | ||||
|         verbose_name_plural = _("guests") | ||||
|         unique_together = ("activity", "last_name", "first_name", ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.first_name + " " + self.last_name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, force_insert=False, force_update=False, using=None, update_fields=None): | ||||
| @@ -290,13 +291,14 @@ class Guest(models.Model): | ||||
|  | ||||
|         return super().save(force_insert, force_update, using, update_fields) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.first_name + " " + self.last_name | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("guest") | ||||
|         verbose_name_plural = _("guests") | ||||
|         unique_together = ("activity", "last_name", "first_name", ) | ||||
|     @property | ||||
|     def has_entry(self): | ||||
|         try: | ||||
|             if self.entry: | ||||
|                 return True | ||||
|             return False | ||||
|         except AttributeError: | ||||
|             return False | ||||
|  | ||||
|  | ||||
| class GuestTransaction(Transaction): | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.utils import timezone | ||||
|   | ||||
| @@ -18,3 +18,26 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|   </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
| <script> | ||||
|   var date_end = document.getElementById("id_date_end"); | ||||
|   var date_start = document.getElementById("id_date_start"); | ||||
|    | ||||
|   function update_date_end (){ | ||||
|     if(date_end.value=="" || date_end.value<date_start.value){ | ||||
|       date_end.value = date_start.value; | ||||
|     }; | ||||
|   }; | ||||
|    | ||||
|   function update_date_start (){ | ||||
|     if(date_start.value=="" || date_end.value<date_start.value){ | ||||
|       date_start.value = date_end.value; | ||||
|     }; | ||||
|   }; | ||||
|    | ||||
|   date_start.addEventListener('focusout', update_date_end); | ||||
|   date_end.addEventListener('focusout', update_date_start); | ||||
|    | ||||
| </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import timedelta | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.urls import path | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from hashlib import md5 | ||||
| @@ -17,7 +17,8 @@ from django.utils.translation import gettext_lazy as _ | ||||
| from django.views import View | ||||
| from django.views.decorators.cache import cache_page | ||||
| from django.views.generic import DetailView, TemplateView, UpdateView | ||||
| from django_tables2.views import SingleTableView | ||||
| from django.views.generic.list import ListView | ||||
| from django_tables2.views import MultiTableMixin | ||||
| from note.models import Alias, NoteSpecial, NoteUser | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin, ProtectedCreateView | ||||
| @@ -57,26 +58,40 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) | ||||
|  | ||||
|  | ||||
| class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
| class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): | ||||
|     """ | ||||
|     Displays all Activities, and classify if they are on-going or upcoming ones. | ||||
|     """ | ||||
|     model = Activity | ||||
|     table_class = ActivityTable | ||||
|     ordering = ('-date_start',) | ||||
|     tables = [ActivityTable, ActivityTable] | ||||
|     extra_context = {"title": _("Activities")} | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset(**kwargs).distinct() | ||||
|  | ||||
|     def get_tables(self): | ||||
|         tables = super().get_tables() | ||||
|  | ||||
|         tables[0].prefix = "all-" | ||||
|         tables[1].prefix = "upcoming-" | ||||
|         return tables | ||||
|  | ||||
|     def get_tables_data(self): | ||||
|         # first table = all activities, second table = upcoming | ||||
|         return [ | ||||
|             self.get_queryset().order_by("-date_start"), | ||||
|             Activity.objects.filter(date_end__gt=timezone.now()) | ||||
|                             .filter(PermissionBackend.filter_queryset(self.request, Activity, "view")) | ||||
|                             .distinct() | ||||
|                             .order_by("date_start") | ||||
|         ] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|  | ||||
|         upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now()) | ||||
|         context['upcoming'] = ActivityTable( | ||||
|             data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")), | ||||
|             prefix='upcoming-', | ||||
|         ) | ||||
|         tables = context["tables"] | ||||
|         for name, table in zip(["table", "upcoming"], tables): | ||||
|             context[name] = table | ||||
|  | ||||
|         started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all() | ||||
|         context["started_activities"] = started_activities | ||||
| @@ -315,8 +330,8 @@ X-WR-CALNAME:Kfet Calendar | ||||
| NAME:Kfet Calendar | ||||
| CALSCALE:GREGORIAN | ||||
| BEGIN:VTIMEZONE | ||||
| TZID:Europe/Berlin | ||||
| X-LIC-LOCATION:Europe/Berlin | ||||
| TZID:Europe/Paris | ||||
| X-LIC-LOCATION:Europe/Paris | ||||
| BEGIN:DAYLIGHT | ||||
| TZOFFSETFROM:+0100 | ||||
| TZOFFSETTO:+0200 | ||||
| @@ -338,10 +353,10 @@ END:VTIMEZONE | ||||
| DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z | ||||
| UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()} | ||||
| SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)} | ||||
| DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)} | ||||
| DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)} | ||||
| DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)} | ||||
| DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)} | ||||
| LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"} | ||||
| DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """ | ||||
| DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f""" | ||||
|  -- {activity.organizer.name} | ||||
| END:VEVENT | ||||
| """ | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'api.apps.APIConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
							
								
								
									
										5
									
								
								apps/api/pagination.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/api/pagination.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| from rest_framework.pagination import PageNumberPagination | ||||
|  | ||||
|  | ||||
| class CustomPagination(PageNumberPagination): | ||||
|     page_size_query_param = 'page_size' | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import json | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.auth.models import User | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'logs.apps.LogsConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import ChangelogViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -76,9 +76,6 @@ class Changelog(models.Model): | ||||
|         verbose_name=_('timestamp'), | ||||
|     ) | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         raise ValidationError(_("Logs cannot be destroyed.")) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("changelog") | ||||
|         verbose_name_plural = _("changelogs") | ||||
| @@ -86,3 +83,6 @@ class Changelog(models.Model): | ||||
|     def __str__(self): | ||||
|         return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format( | ||||
|             action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp)) | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         raise ValidationError(_("Logs cannot be destroyed.")) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| @@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs): | ||||
|     # noinspection PyProtectedMember | ||||
|     previous = instance._previous | ||||
|  | ||||
|     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP | ||||
|     # Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP | ||||
|     request = get_current_request() | ||||
|  | ||||
|     if request is None: | ||||
|         # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` | ||||
|         # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée | ||||
|         # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info | ||||
|         # IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info | ||||
|         ip = "127.0.0.1" | ||||
|         username = Alias.normalize(getpass.getuser()) | ||||
|         note = NoteUser.objects.filter(alias__normalized_name=username) | ||||
| @@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs): | ||||
|     if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"): | ||||
|         return | ||||
|  | ||||
|     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP | ||||
|     # Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP | ||||
|     request = get_current_request() | ||||
|  | ||||
|     if request is None: | ||||
|         # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` | ||||
|         # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée | ||||
|         # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info | ||||
|         # IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info | ||||
|         ip = "127.0.0.1" | ||||
|         username = Alias.normalize(getpass.getuser()) | ||||
|         note = NoteUser.objects.filter(alias__normalized_name=username) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'member.apps.MemberConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib import admin | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import ProfileViewSet, ClubViewSet, MembershipViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from cas_server.auth import DjangoAuthUser  # pragma: no cover | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import io | ||||
| @@ -47,6 +47,13 @@ class ProfileForm(forms.ModelForm): | ||||
|  | ||||
|     last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date")) | ||||
|  | ||||
|     VSS_charter_read = forms.BooleanField( | ||||
|         required=True, | ||||
|         label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"), | ||||
|         help_text=_("Tick after having read and accepted the anti-VSS charter \ | ||||
|         <a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>") | ||||
|     ) | ||||
|  | ||||
|     def clean_promotion(self): | ||||
|         promotion = self.cleaned_data["promotion"] | ||||
|         if promotion > timezone.now().year: | ||||
| @@ -114,7 +121,7 @@ class ImageForm(forms.Form): | ||||
|                 frame = frame.crop((x, y, x + w, y + h)) | ||||
|                 frame = frame.resize( | ||||
|                     (settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH), | ||||
|                     Image.ANTIALIAS, | ||||
|                     Image.LANCZOS, | ||||
|                 ) | ||||
|                 frames.append(frame) | ||||
|  | ||||
| @@ -131,6 +138,9 @@ class ImageForm(forms.Form): | ||||
|  | ||||
|         return cleaned_data | ||||
|  | ||||
|     def is_valid(self): | ||||
|         return super().is_valid() or super().clean().get('image') is None | ||||
|  | ||||
|  | ||||
| class ClubForm(forms.ModelForm): | ||||
|     def clean(self): | ||||
| @@ -144,7 +154,7 @@ class ClubForm(forms.ModelForm): | ||||
|  | ||||
|     class Meta: | ||||
|         model = Club | ||||
|         fields = '__all__' | ||||
|         exclude = ("add_registration_form",) | ||||
|         widgets = { | ||||
|             "membership_fee_paid": AmountInput(), | ||||
|             "membership_fee_unpaid": AmountInput(), | ||||
| @@ -200,9 +210,9 @@ class MembershipForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = Membership | ||||
|         fields = ('user', 'date_start') | ||||
|         # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. | ||||
|         # Le champ d'utilisateur⋅rice 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 | ||||
|         # et récupère les noms d'utilisateur⋅rices valides | ||||
|         widgets = { | ||||
|             'user': | ||||
|                 Autocomplete( | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import hashlib | ||||
|   | ||||
							
								
								
									
										18
									
								
								apps/member/migrations/0009_auto_20220904_2325.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/member/migrations/0009_auto_20220904_2325.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.2.26 on 2022-09-04 21:25 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('member', '0008_auto_20211005_1544'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='profile', | ||||
|             name='promotion', | ||||
|             field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								apps/member/migrations/0010_new_default_year.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/member/migrations/0010_new_default_year.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.2.28 on 2023-08-23 21:29 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('member', '0009_auto_20220904_2325'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='profile', | ||||
|             name='promotion', | ||||
|             field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								apps/member/migrations/0011_profile_vss_charter_read.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/member/migrations/0011_profile_vss_charter_read.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.2.28 on 2023-08-31 09:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('member', '0010_new_default_year'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='profile', | ||||
|             name='VSS_charter_read', | ||||
|             field=models.BooleanField(default=False, verbose_name='VSS charter read'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								apps/member/migrations/0012_club_add_registration_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/member/migrations/0012_club_add_registration_form.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.2.28 on 2024-07-15 09:24 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('member', '0011_profile_vss_charter_read'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='club', | ||||
|             name='add_registration_form', | ||||
|             field=models.BooleanField(default=False, verbose_name='add to registration form'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import datetime | ||||
| @@ -28,7 +28,6 @@ class Profile(models.Model): | ||||
|     We do not want to patch the Django Contrib :model:`auth.User`model; | ||||
|     so this model add an user profile with additional information. | ||||
|     """ | ||||
|  | ||||
|     user = models.OneToOneField( | ||||
|         settings.AUTH_USER_MODEL, | ||||
|         on_delete=models.CASCADE, | ||||
| @@ -134,6 +133,22 @@ class Profile(models.Model): | ||||
|         default=False, | ||||
|     ) | ||||
|  | ||||
|     VSS_charter_read = models.BooleanField( | ||||
|         verbose_name=_("VSS charter read"), | ||||
|         default=False | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('user profile') | ||||
|         verbose_name_plural = _('user profile') | ||||
|         indexes = [models.Index(fields=['user'])] | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.user) | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('member:user_detail', args=(self.user_id,)) | ||||
|  | ||||
|     @property | ||||
|     def ens_year(self): | ||||
|         """ | ||||
| @@ -158,17 +173,6 @@ class Profile(models.Model): | ||||
|             return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists() | ||||
|         return False | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('user profile') | ||||
|         verbose_name_plural = _('user profile') | ||||
|         indexes = [models.Index(fields=['user'])] | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('member:user_detail', args=(self.user_id,)) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.user) | ||||
|  | ||||
|     def send_email_validation_link(self): | ||||
|         subject = "[Note Kfet] " + str(_("Activate your Note Kfet account")) | ||||
|         token = email_validation_token.make_token(self.user) | ||||
| @@ -200,9 +204,11 @@ class Club(models.Model): | ||||
|         max_length=255, | ||||
|         unique=True, | ||||
|     ) | ||||
|  | ||||
|     email = models.EmailField( | ||||
|         verbose_name=_('email'), | ||||
|     ) | ||||
|  | ||||
|     parent_club = models.ForeignKey( | ||||
|         'self', | ||||
|         null=True, | ||||
| @@ -253,25 +259,17 @@ class Club(models.Model): | ||||
|         help_text=_('Maximal date of a membership, after which members must renew it.'), | ||||
|     ) | ||||
|  | ||||
|     def update_membership_dates(self): | ||||
|         """ | ||||
|         This function is called each time the club detail view is displayed. | ||||
|         Update the year of the membership dates. | ||||
|         """ | ||||
|         if not self.membership_start or not self.membership_end: | ||||
|             return | ||||
|     add_registration_form = models.BooleanField( | ||||
|         verbose_name=_("add to registration form"), | ||||
|         default=False, | ||||
|     ) | ||||
|  | ||||
|         today = datetime.date.today() | ||||
|     class Meta: | ||||
|         verbose_name = _("club") | ||||
|         verbose_name_plural = _("clubs") | ||||
|  | ||||
|         if (today - self.membership_start).days >= 365: | ||||
|             if self.membership_start: | ||||
|                 self.membership_start = datetime.date(self.membership_start.year + 1, | ||||
|                                                       self.membership_start.month, self.membership_start.day) | ||||
|             if self.membership_end: | ||||
|                 self.membership_end = datetime.date(self.membership_end.year + 1, | ||||
|                                                     self.membership_end.month, self.membership_end.day) | ||||
|             self._force_save = True | ||||
|             self.save(force_update=True) | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, force_insert=False, force_update=False, using=None, | ||||
| @@ -284,16 +282,36 @@ class Club(models.Model): | ||||
|             self.membership_end = None | ||||
|         super().save(force_insert, force_update, update_fields) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("club") | ||||
|         verbose_name_plural = _("clubs") | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse_lazy('member:club_detail', args=(self.pk,)) | ||||
|  | ||||
|     def update_membership_dates(self): | ||||
|         """ | ||||
|         This function is called each time the club detail view is displayed. | ||||
|         Update the year of the membership dates. | ||||
|         """ | ||||
|         if not self.membership_start or not self.membership_end: | ||||
|             return | ||||
|  | ||||
|         today = datetime.date.today() | ||||
|  | ||||
|         # Avoid any problems on February 29 | ||||
|         if self.membership_start.month == 2 and self.membership_start.day == 29: | ||||
|             self.membership_start -= datetime.timedelta(days=1) | ||||
|         if self.membership_end.month == 2 and self.membership_end.day == 29: | ||||
|             self.membership_end += datetime.timedelta(days=1) | ||||
|  | ||||
|         while today >= datetime.date(self.membership_start.year + 1, | ||||
|                                      self.membership_start.month, self.membership_start.day): | ||||
|             if self.membership_start: | ||||
|                 self.membership_start = datetime.date(self.membership_start.year + 1, | ||||
|                                                       self.membership_start.month, self.membership_start.day) | ||||
|             if self.membership_end: | ||||
|                 self.membership_end = datetime.date(self.membership_end.year + 1, | ||||
|                                                     self.membership_end.month, self.membership_end.day) | ||||
|             self._force_save = True | ||||
|             self.save(force_update=True) | ||||
|  | ||||
|  | ||||
| class Membership(models.Model): | ||||
|     """ | ||||
| @@ -333,6 +351,66 @@ class Membership(models.Model): | ||||
|         verbose_name=_('fee'), | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('membership') | ||||
|         verbose_name_plural = _('memberships') | ||||
|         indexes = [models.Index(fields=['user'])] | ||||
|  | ||||
|     def __str__(self): | ||||
|         return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         Calculate fee and end date before saving the membership and creating the transaction if needed. | ||||
|         """ | ||||
|         # Ensure that club membership dates are valid | ||||
|         old_membership_start = self.club.membership_start | ||||
|         self.club.update_membership_dates() | ||||
|         if self.club.membership_start != old_membership_start: | ||||
|             self.club.save() | ||||
|  | ||||
|         created = not self.pk | ||||
|         if not created: | ||||
|             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)) | ||||
|         else: | ||||
|             if Membership.objects.filter( | ||||
|                     user=self.user, | ||||
|                     club=self.club, | ||||
|                     date_start__lte=self.date_start, | ||||
|                     date_end__gte=self.date_start, | ||||
|             ).exists(): | ||||
|                 raise ValidationError(_('User is already a member of the club')) | ||||
|  | ||||
|             if self.club.parent_club is not None: | ||||
|                 # Check that the user is already a member of the parent club if the membership is created | ||||
|                 if not Membership.objects.filter( | ||||
|                     user=self.user, | ||||
|                     club=self.club.parent_club, | ||||
|                     date_start__gte=self.club.parent_club.membership_start, | ||||
|                 ).exists(): | ||||
|                     if hasattr(self, '_force_renew_parent') and self._force_renew_parent: | ||||
|                         self.renew_parent() | ||||
|                     else: | ||||
|                         raise ValidationError(_('User is not a member of the parent club') | ||||
|                                               + ' ' + self.club.parent_club.name) | ||||
|  | ||||
|             self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid | ||||
|  | ||||
|             self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \ | ||||
|                 if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242) | ||||
|             if self.club.membership_end is not None and self.date_end > self.club.membership_end: | ||||
|                 self.date_end = self.club.membership_end | ||||
|  | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         self.make_transaction() | ||||
|  | ||||
|     @property | ||||
|     def valid(self): | ||||
|         """ | ||||
| @@ -402,66 +480,14 @@ class Membership(models.Model): | ||||
|  | ||||
|             if self.club.parent_club.name == "BDE": | ||||
|                 parent_membership.roles.set( | ||||
|                     Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all()) | ||||
|                     Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all()) | ||||
|             elif self.club.parent_club.name == "Kfet": | ||||
|                 parent_membership.roles.set( | ||||
|                     Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all()) | ||||
|                     Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all()) | ||||
|             else: | ||||
|                 parent_membership.roles.set(Role.objects.filter(name="Membre de club").all()) | ||||
|             parent_membership.save() | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         Calculate fee and end date before saving the membership and creating the transaction if needed. | ||||
|         """ | ||||
|         # Ensure that club membership dates are valid | ||||
|         old_membership_start = self.club.membership_start | ||||
|         self.club.update_membership_dates() | ||||
|         if self.club.membership_start != old_membership_start: | ||||
|             self.club.save() | ||||
|  | ||||
|         created = not self.pk | ||||
|         if not created: | ||||
|             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)) | ||||
|         else: | ||||
|             if Membership.objects.filter( | ||||
|                     user=self.user, | ||||
|                     club=self.club, | ||||
|                     date_start__lte=self.date_start, | ||||
|                     date_end__gte=self.date_start, | ||||
|             ).exists(): | ||||
|                 raise ValidationError(_('User is already a member of the club')) | ||||
|  | ||||
|             if self.club.parent_club is not None: | ||||
|                 # Check that the user is already a member of the parent club if the membership is created | ||||
|                 if not Membership.objects.filter( | ||||
|                     user=self.user, | ||||
|                     club=self.club.parent_club, | ||||
|                     date_start__gte=self.club.parent_club.membership_start, | ||||
|                 ).exists(): | ||||
|                     if hasattr(self, '_force_renew_parent') and self._force_renew_parent: | ||||
|                         self.renew_parent() | ||||
|                     else: | ||||
|                         raise ValidationError(_('User is not a member of the parent club') | ||||
|                                               + ' ' + self.club.parent_club.name) | ||||
|  | ||||
|             self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid | ||||
|  | ||||
|             self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \ | ||||
|                 if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242) | ||||
|             if self.club.membership_end is not None and self.date_end > self.club.membership_end: | ||||
|                 self.date_end = self.club.membership_end | ||||
|  | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         self.make_transaction() | ||||
|  | ||||
|     def make_transaction(self): | ||||
|         """ | ||||
|         Create Membership transaction associated to this membership. | ||||
| @@ -499,11 +525,3 @@ class Membership(models.Model): | ||||
|                 soge_credit.save() | ||||
|             else: | ||||
|                 transaction.save(force_insert=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('membership') | ||||
|         verbose_name_plural = _('memberships') | ||||
|         indexes = [models.Index(fields=['user'])] | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| /** | ||||
|  * On form submit, create a new friendship | ||||
|  */ | ||||
| function create_trust (e) { | ||||
| function form_create_trust (e) { | ||||
|   // Do not submit HTML form | ||||
|   e.preventDefault() | ||||
|  | ||||
| @@ -14,25 +14,35 @@ function create_trust (e) { | ||||
|          addMsg(gettext("You can't add yourself as a friend"), "danger") | ||||
|          return | ||||
|       } | ||||
|       $.post('/api/note/trust/', { | ||||
|         csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'), | ||||
|         trusting: formData.get('trusting'), | ||||
|         trusted: trusted_alias.note | ||||
|       }).done(function () { | ||||
|         // Reload table | ||||
|         $('#trust_table').load(location.pathname + ' #trust_table') | ||||
|         addMsg(gettext('Friendship successfully added'), 'success') | ||||
|       }).fail(function (xhr, _textStatus, _error) { | ||||
|         errMsg(xhr.responseJSON) | ||||
|       }) | ||||
|       create_trust(formData.get('trusting'), trusted_alias.note) | ||||
|     }).fail(function (xhr, _textStatus, _error) { | ||||
|         errMsg(xhr.responseJSON) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * On click of "delete", delete the alias | ||||
|  * @param button_id:Integer Alias id to remove | ||||
|  * Create a trust between users | ||||
|  * @param trusting:Integer trusting note id | ||||
|  * @param trusted:Integer trusted note id | ||||
|  */ | ||||
| function create_trust(trusting, trusted) { | ||||
|   $.post('/api/note/trust/', { | ||||
|       trusting: trusting, | ||||
|       trusted: trusted, | ||||
|       csrfmiddlewaretoken: CSRF_TOKEN | ||||
|   }).done(function () { | ||||
|   // Reload tables | ||||
|   $('#trust_table').load(location.pathname + ' #trust_table') | ||||
|   $('#trusted_table').load(location.pathname + ' #trusted_table') | ||||
|     addMsg(gettext('Friendship successfully added'), 'success') | ||||
|   }).fail(function (xhr, _textStatus, _error) { | ||||
|     errMsg(xhr.responseJSON) | ||||
|   }) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * On click of "delete", delete the trust | ||||
|  * @param button_id:Integer Trust id to remove | ||||
|  */ | ||||
| function delete_button (button_id) { | ||||
|   $.ajax({ | ||||
| @@ -42,6 +52,7 @@ function delete_button (button_id) { | ||||
|   }).done(function () { | ||||
|     addMsg(gettext('Friendship successfully deleted'), 'success') | ||||
|     $('#trust_table').load(location.pathname + ' #trust_table') | ||||
|     $('#trusted_table').load(location.pathname + ' #trusted_table') | ||||
|   }).fail(function (xhr, _textStatus, _error) { | ||||
|     errMsg(xhr.responseJSON) | ||||
|   }) | ||||
| @@ -49,5 +60,5 @@ function delete_button (button_id) { | ||||
|  | ||||
| $(document).ready(function () { | ||||
|   // Attach event | ||||
|   document.getElementById('form_trust').addEventListener('submit', create_trust) | ||||
|   document.getElementById('form_trust').addEventListener('submit', form_create_trust) | ||||
| }) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
|   | ||||
| @@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|       <form method="post" enctype="multipart/form-data" id="formUpload"> | ||||
|         {% csrf_token %} | ||||
|         {{ form |crispy }} | ||||
|         {% if user.note.display_image != "pic/default.png" %} | ||||
|           <input type="submit" class="btn btn-primary" value="{% trans "Remove" %}"> | ||||
|         {% endif %} | ||||
|       </form> | ||||
|     </div> | ||||
|     <!-- MODAL TO CROP THE IMAGE --> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% block profile_content %} | ||||
| <div class="card bg-light mb-3"> | ||||
|     <h3 class="card-header text-center"> | ||||
|         {% trans "Note friendships" %} | ||||
|         {% trans "Add friends" %} | ||||
|     </h3> | ||||
|     <div class="card-body"> | ||||
|         {% if can_create %} | ||||
| @@ -24,7 +24,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     {% render_table trusting %} | ||||
| </div> | ||||
|  | ||||
| <div class="alert alert-warning card"> | ||||
| <div class="alert alert-warning card mb-3"> | ||||
|     {% blocktrans trimmed %} | ||||
|         Adding someone as a friend enables them to initiate transactions coming | ||||
|         from your account (while keeping your balance positive). This is | ||||
| @@ -33,6 +33,13 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         friends without needing additional rights among them. | ||||
|     {% endblocktrans %} | ||||
| </div> | ||||
|  | ||||
| <div class="card bg-light mb-3"> | ||||
|     <h3 class="card-header text-center"> | ||||
|         {% trans "People having you as a friend" %} | ||||
|     </h3> | ||||
|     {% render_table trusted_by %} | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
|   | ||||
| @@ -183,7 +183,7 @@ class TestMemberships(TestCase): | ||||
|                 club = Club.objects.get(name="Kfet") | ||||
|             else: | ||||
|                 club = Club.objects.create( | ||||
|                     name="Second club " + ("with BDE" if bde_parent else "without BDE"), | ||||
|                     name="Second club without BDE", | ||||
|                     parent_club=None, | ||||
|                     email="newclub@example.com", | ||||
|                     require_memberships=True, | ||||
| @@ -291,7 +291,7 @@ class TestMemberships(TestCase): | ||||
|  | ||||
|         response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict( | ||||
|             roles=[role.id for role in Role.objects.filter( | ||||
|                 Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()], | ||||
|                 Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()], | ||||
|         )) | ||||
|         self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) | ||||
|         self.membership.refresh_from_db() | ||||
| @@ -335,6 +335,7 @@ class TestMemberships(TestCase): | ||||
|             ml_sports_registration=True, | ||||
|             ml_art_registration=True, | ||||
|             report_frequency=7, | ||||
|             VSS_charter_read=True | ||||
|         )) | ||||
|         self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) | ||||
|         self.assertTrue(User.objects.filter(username="toto changed").exists()) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.urls import path | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import timedelta, date | ||||
| @@ -8,7 +8,6 @@ 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 | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.db import transaction | ||||
| from django.db.models import Q, F | ||||
| from django.shortcuts import redirect | ||||
| @@ -21,7 +20,7 @@ from django_tables2.views import SingleTableView | ||||
| from rest_framework.authtoken.models import Token | ||||
| from note.models import Alias, NoteClub, NoteUser, Trust | ||||
| from note.models.transactions import Transaction, SpecialTransaction | ||||
| from note.tables import HistoryTable, AliasTable, TrustTable | ||||
| from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable | ||||
| from note_kfet.middlewares import _set_current_request | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.models import Role | ||||
| @@ -258,17 +257,18 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|         note = context['object'].note | ||||
|         context["trusting"] = TrustTable( | ||||
|             note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all()) | ||||
|         context["trusted_by"] = TrustedTable( | ||||
|             note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all()) | ||||
|         context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust( | ||||
|             trusting=context["object"].note, | ||||
|             trusted=context["object"].note | ||||
|         )) | ||||
|         context["widget"] = { | ||||
|             "name": "trusted", | ||||
|             "resetable": True, | ||||
|             "attrs": { | ||||
|                 "model_pk": ContentType.objects.get_for_model(Alias).pk, | ||||
|                 "class": "autocomplete form-control", | ||||
|                 "id": "trusted", | ||||
|                 "resetable": True, | ||||
|                 "api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser", | ||||
|                 "name_field": "name", | ||||
|                 "placeholder": "" | ||||
| @@ -326,6 +326,9 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det | ||||
|         """Save image to note""" | ||||
|         image = form.cleaned_data['image'] | ||||
|  | ||||
|         if image is None: | ||||
|             image = "pic/default.png" | ||||
|         else: | ||||
|             # Rename as a PNG or GIF | ||||
|             extension = image.name.split(".")[-1] | ||||
|             if extension == "gif": | ||||
| @@ -753,6 +756,10 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             club = old_membership.club | ||||
|             user = old_membership.user | ||||
|  | ||||
|         # Update club membership date | ||||
|         if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club): | ||||
|             club.update_membership_dates() | ||||
|  | ||||
|         form.instance.club = club | ||||
|  | ||||
|         # Get form data | ||||
| @@ -820,8 +827,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|  | ||||
|         ret = super().form_valid(form) | ||||
|  | ||||
|         member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \ | ||||
|             if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \ | ||||
|         member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \ | ||||
|             if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \ | ||||
|             if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() | ||||
|         # Set the same roles as before | ||||
|         if old_membership: | ||||
| @@ -857,7 +864,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|                 membership.refresh_from_db() | ||||
|                 if old_membership.exists(): | ||||
|                     membership.roles.set(old_membership.get().roles.all()) | ||||
|                 membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all()) | ||||
|                 membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all()) | ||||
|                 membership.save() | ||||
|  | ||||
|         return ret | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'note.apps.NoteConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib import admin | ||||
| @@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ | ||||
|     PolymorphicChildModelFilter, PolymorphicParentModelAdmin | ||||
| from note_kfet.admin import admin_site | ||||
|  | ||||
| from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser | ||||
| from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust | ||||
| from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ | ||||
|     RecurrentTransaction, MembershipTransaction, SpecialTransaction | ||||
| from .templatetags.pretty_money import pretty_money | ||||
| @@ -21,6 +21,16 @@ class AliasInlines(admin.TabularInline): | ||||
|     model = Alias | ||||
|  | ||||
|  | ||||
| class TrustInlines(admin.TabularInline): | ||||
|     """ | ||||
|     Define trusts when editing the trusting note | ||||
|     """ | ||||
|     model = Trust | ||||
|     fk_name = "trusting" | ||||
|     extra = 0 | ||||
|     readonly_fields = ("trusted",) | ||||
|  | ||||
|  | ||||
| @admin.register(Note, site=admin_site) | ||||
| class NoteAdmin(PolymorphicParentModelAdmin): | ||||
|     """ | ||||
| @@ -92,7 +102,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin): | ||||
|     """ | ||||
|     Child for an user note, see NoteAdmin | ||||
|     """ | ||||
|     inlines = (AliasInlines,) | ||||
|     inlines = (AliasInlines, TrustInlines) | ||||
|  | ||||
|     # We can't change user after creation or the balance | ||||
|     readonly_fields = ('user', 'balance') | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| @@ -11,6 +11,7 @@ from member.models import Membership | ||||
| from note_kfet.middlewares import get_current_request | ||||
| from permission.backends import PermissionBackend | ||||
| from rest_framework.utils import model_meta | ||||
| from rest_framework.validators import UniqueTogetherValidator | ||||
|  | ||||
| from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust | ||||
| from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ | ||||
| @@ -86,11 +87,9 @@ class TrustSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Trust | ||||
|         fields = '__all__' | ||||
|  | ||||
|     def validate(self, attrs): | ||||
|         instance = Trust(**attrs) | ||||
|         instance.clean() | ||||
|         return attrs | ||||
|         validators = [UniqueTogetherValidator( | ||||
|             queryset=Trust.objects.all(), fields=('trusting', 'trusted'), | ||||
|             message=_("This friendship already exists"))] | ||||
|  | ||||
|  | ||||
| class AliasSerializer(serializers.ModelSerializer): | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \ | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| import re | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| from datetime import datetime | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import unicodedata | ||||
| @@ -293,6 +293,11 @@ class Alias(models.Model): | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def normalize(string): | ||||
|         """ | ||||
| @@ -321,11 +326,6 @@ class Alias(models.Model): | ||||
|             pass | ||||
|         self.normalized_name = normalized_name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     def delete(self, using=None, keep_parents=False): | ||||
|         if self.name == str(self.note): | ||||
|             raise ValidationError(_("You can't delete your main alias."), | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.core.exceptions import ValidationError | ||||
| @@ -59,6 +59,7 @@ class TransactionTemplate(models.Model): | ||||
|     amount = models.PositiveIntegerField( | ||||
|         verbose_name=_('amount'), | ||||
|     ) | ||||
|  | ||||
|     category = models.ForeignKey( | ||||
|         TemplateCategory, | ||||
|         on_delete=models.PROTECT, | ||||
| @@ -87,12 +88,12 @@ class TransactionTemplate(models.Model): | ||||
|         verbose_name = _("transaction template") | ||||
|         verbose_name_plural = _("transaction templates") | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('note:template_update', args=(self.pk,)) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     def get_absolute_url(self): | ||||
|         return reverse('note:template_update', args=(self.pk,)) | ||||
|  | ||||
|  | ||||
| class Transaction(PolymorphicModel): | ||||
|     """ | ||||
| @@ -101,7 +102,6 @@ class Transaction(PolymorphicModel): | ||||
|     amount is store in centimes of currency, making it a  positive integer | ||||
|     value. (from someone to someone else) | ||||
|     """ | ||||
|  | ||||
|     source = models.ForeignKey( | ||||
|         Note, | ||||
|         on_delete=models.PROTECT, | ||||
| @@ -166,6 +166,50 @@ class Transaction(PolymorphicModel): | ||||
|             models.Index(fields=['destination']), | ||||
|         ] | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\ | ||||
|             + pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid") | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         When saving, also transfer money between two notes | ||||
|         """ | ||||
|         if self.source.pk == self.destination.pk: | ||||
|             # When source == destination, no money is transferred and no transaction is created | ||||
|             return | ||||
|  | ||||
|         self.source = Note.objects.select_for_update().get(pk=self.source_id) | ||||
|         self.destination = Note.objects.select_for_update().get(pk=self.destination_id) | ||||
|  | ||||
|         # Check that the amounts stay between big integer bounds | ||||
|         diff_source, diff_dest = self.validate() | ||||
|  | ||||
|         if not (hasattr(self, '_force_save') and self._force_save) \ | ||||
|                 and (not self.source.is_active or not self.destination.is_active): | ||||
|             raise ValidationError(_("The transaction can't be saved since the source note " | ||||
|                                     "or the destination note is not active.")) | ||||
|  | ||||
|         # If the aliases are not entered, we assume that the used alias is the name of the note | ||||
|         if not self.source_alias: | ||||
|             self.source_alias = str(self.source) | ||||
|  | ||||
|         if not self.destination_alias: | ||||
|             self.destination_alias = str(self.destination) | ||||
|  | ||||
|         # We save first the transaction, in case of the user has no right to transfer money | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         # Save notes | ||||
|         self.source.refresh_from_db() | ||||
|         self.source.balance += diff_source | ||||
|         self.source._force_save = True | ||||
|         self.source.save() | ||||
|         self.destination.refresh_from_db() | ||||
|         self.destination.balance += diff_dest | ||||
|         self.destination._force_save = True | ||||
|         self.destination.save() | ||||
|  | ||||
|     def validate(self): | ||||
|         previous_source_balance = self.source.balance | ||||
|         previous_dest_balance = self.destination.balance | ||||
| @@ -208,46 +252,6 @@ class Transaction(PolymorphicModel): | ||||
|  | ||||
|         return source_balance - previous_source_balance, dest_balance - previous_dest_balance | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         When saving, also transfer money between two notes | ||||
|         """ | ||||
|         if self.source.pk == self.destination.pk: | ||||
|             # When source == destination, no money is transferred and no transaction is created | ||||
|             return | ||||
|  | ||||
|         self.source = Note.objects.select_for_update().get(pk=self.source_id) | ||||
|         self.destination = Note.objects.select_for_update().get(pk=self.destination_id) | ||||
|  | ||||
|         # Check that the amounts stay between big integer bounds | ||||
|         diff_source, diff_dest = self.validate() | ||||
|  | ||||
|         if not (hasattr(self, '_force_save') and self._force_save) \ | ||||
|                 and (not self.source.is_active or not self.destination.is_active): | ||||
|             raise ValidationError(_("The transaction can't be saved since the source note " | ||||
|                                     "or the destination note is not active.")) | ||||
|  | ||||
|         # If the aliases are not entered, we assume that the used alias is the name of the note | ||||
|         if not self.source_alias: | ||||
|             self.source_alias = str(self.source) | ||||
|  | ||||
|         if not self.destination_alias: | ||||
|             self.destination_alias = str(self.destination) | ||||
|  | ||||
|         # We save first the transaction, in case of the user has no right to transfer money | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         # Save notes | ||||
|         self.source.refresh_from_db() | ||||
|         self.source.balance += diff_source | ||||
|         self.source._force_save = True | ||||
|         self.source.save() | ||||
|         self.destination.refresh_from_db() | ||||
|         self.destination.balance += diff_dest | ||||
|         self.destination._force_save = True | ||||
|         self.destination.save() | ||||
|  | ||||
|     @property | ||||
|     def total(self): | ||||
|         return self.amount * self.quantity | ||||
| @@ -256,46 +260,40 @@ class Transaction(PolymorphicModel): | ||||
|     def type(self): | ||||
|         return _('Transfer') | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\ | ||||
|             + pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid") | ||||
|  | ||||
|  | ||||
| class RecurrentTransaction(Transaction): | ||||
|     """ | ||||
|     Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. | ||||
|     """ | ||||
|  | ||||
|     template = models.ForeignKey( | ||||
|         TransactionTemplate, | ||||
|         on_delete=models.PROTECT, | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("recurrent transaction") | ||||
|         verbose_name_plural = _("recurrent transactions") | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     def clean(self): | ||||
|         if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save): | ||||
|             raise ValidationError( | ||||
|                 _("The destination of this transaction must equal to the destination of the template.")) | ||||
|         return super().clean() | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def type(self): | ||||
|         return _('Template') | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("recurrent transaction") | ||||
|         verbose_name_plural = _("recurrent transactions") | ||||
|  | ||||
|  | ||||
| class SpecialTransaction(Transaction): | ||||
|     """ | ||||
|     Special type of :model:`note.Transaction` associated to transactions with special notes | ||||
|     """ | ||||
|  | ||||
|     last_name = models.CharField( | ||||
|         max_length=255, | ||||
|         verbose_name=_("name"), | ||||
| @@ -312,6 +310,15 @@ class SpecialTransaction(Transaction): | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("Special transaction") | ||||
|         verbose_name_plural = _("Special transactions") | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def type(self): | ||||
|         return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit") | ||||
| @@ -325,13 +332,8 @@ class SpecialTransaction(Transaction): | ||||
|     def clean(self): | ||||
|         # SpecialTransaction are only possible with NoteSpecial object | ||||
|         if self.is_credit() == self.is_debit(): | ||||
|             raise(ValidationError(_("A special transaction is only possible between a" | ||||
|                                     " Note associated to a payment method and a User or a Club"))) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         super().save(*args, **kwargs) | ||||
|             raise ValidationError(_("A special transaction is only possible between a" | ||||
|                                     " Note associated to a payment method and a User or a Club")) | ||||
|  | ||||
|     @staticmethod | ||||
|     def validate_payment_form(form): | ||||
| @@ -363,17 +365,11 @@ class SpecialTransaction(Transaction): | ||||
|  | ||||
|         return not error | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("Special transaction") | ||||
|         verbose_name_plural = _("Special transactions") | ||||
|  | ||||
|  | ||||
| class MembershipTransaction(Transaction): | ||||
|     """ | ||||
|     Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     membership = models.OneToOneField( | ||||
|         'member.Membership', | ||||
|         on_delete=models.PROTECT, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.utils import timezone | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| // Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| // Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| // When a transaction is performed, lock the interface to prevent spam clicks. | ||||
| @@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca | ||||
|     .done(function () { | ||||
|       if (!isNaN(source.balance)) { | ||||
|         const newBalance = source.balance - quantity * amount | ||||
|         if (newBalance <= -5000) { | ||||
|         if (newBalance <= -2000) { | ||||
|           addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + | ||||
|               'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000) | ||||
|         } else if (newBalance < 0) { | ||||
| @@ -258,3 +258,39 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca | ||||
|       }) | ||||
|     }) | ||||
| } | ||||
|  | ||||
| var searchbar = document.getElementById("search-input") | ||||
| var search_results = document.getElementById("search-results") | ||||
|  | ||||
| var old_pattern = null; | ||||
| var firstMatch = null; | ||||
| /** | ||||
|  * Updates the button search tab | ||||
|  * @param force Forces the update even if the pattern didn't change | ||||
|  */ | ||||
| function updateSearch(force = false) { | ||||
|   let pattern = searchbar.value | ||||
|   if (pattern === "") | ||||
|     firstMatch = null; | ||||
|   if ((pattern === old_pattern || pattern === "") && !force) | ||||
|     return; | ||||
|   firstMatch = null; | ||||
|   const re = new RegExp(pattern, "i"); | ||||
|   Array.from(search_results.children).forEach(function(b) { | ||||
|     if (re.test(b.innerText)) { | ||||
|       b.hidden = false; | ||||
|       if (firstMatch === null) { | ||||
|         firstMatch = b; | ||||
|       } | ||||
|     } else | ||||
|       b.hidden = true; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| searchbar.addEventListener("input", function (e) { | ||||
|   debounce(updateSearch)() | ||||
| }); | ||||
| searchbar.addEventListener("keyup", function (e) { | ||||
|   if (firstMatch && e.key === "Enter") | ||||
|     firstMatch.click() | ||||
| }); | ||||
|   | ||||
| @@ -314,7 +314,7 @@ $('#btn_transfer').click(function () { | ||||
|  | ||||
|           if (!isNaN(source.note.balance)) { | ||||
|             const newBalance = source.note.balance - source.quantity * dest.quantity * amount | ||||
|             if (newBalance <= -5000) { | ||||
|             if (newBalance <= -2000) { | ||||
|               addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'), | ||||
|                   [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000) | ||||
|               reset() | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import html | ||||
| @@ -159,11 +159,11 @@ class TrustTable(tables.Table): | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|  | ||||
|     show_header = False | ||||
|     trusted = tables.Column(attrs={'td': {'class': 'text_center'}}) | ||||
|     trusted = tables.Column(attrs={'td': {'class': 'text-center'}}) | ||||
|  | ||||
|     delete_col = tables.TemplateColumn( | ||||
|         template_code=DELETE_TEMPLATE, | ||||
|         extra_context={"delete_trans": _('delete')}, | ||||
|         extra_context={"delete_trans": _('Delete')}, | ||||
|         attrs={ | ||||
|             'td': { | ||||
|                 'class': lambda record: 'col-sm-1' | ||||
| @@ -173,6 +173,46 @@ class TrustTable(tables.Table): | ||||
|         verbose_name=_("Delete"),) | ||||
|  | ||||
|  | ||||
| class TrustedTable(tables.Table): | ||||
|     class Meta: | ||||
|         attrs = { | ||||
|             'class': 'table table condensed table-striped', | ||||
|             'id': 'trusted_table' | ||||
|         } | ||||
|         Model = Trust | ||||
|         fields = ("trusting",) | ||||
|         template_name = "django_tables2/bootstrap4.html" | ||||
|  | ||||
|     show_header = False | ||||
|     trusting = tables.Column(attrs={ | ||||
|         'td': {'class': 'text-center', 'width': '100%'}}) | ||||
|  | ||||
|     trust_back = tables.Column( | ||||
|         verbose_name=_("Trust back"), | ||||
|         accessor="pk", | ||||
|         attrs={ | ||||
|             'td': { | ||||
|                 'class': '', | ||||
|                 'id': lambda record: "trust_back_" + str(record.pk), | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|     def render_trust_back(self, record): | ||||
|         user_note = record.trusted | ||||
|         trusting_note = record.trusting | ||||
|         if Trust.objects.filter(trusted=trusting_note, trusting=user_note): | ||||
|             return "" | ||||
|         val = '<button id="' | ||||
|         val += str(record.pk) | ||||
|         val += '" class="btn btn-success btn-sm text-nowrap" \ | ||||
|             onclick="create_trust(' + str(record.trusted.pk) + ',' + \ | ||||
|             str(record.trusting.pk) + ')">' | ||||
|         val += str(_("Add back")) | ||||
|         val += '</button>' | ||||
|         return mark_safe(val) | ||||
|  | ||||
|  | ||||
| class AliasTable(tables.Table): | ||||
|     class Meta: | ||||
|         attrs = { | ||||
|   | ||||
| @@ -103,6 +103,11 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                                 </a> | ||||
|                             </li> | ||||
|                         {% endfor %} | ||||
|                             <li class="nav-item"> | ||||
|                                 <a class="nav-link font-weight-bold" data-toggle="tab" href="#search"> | ||||
|                                     {% trans "Search" %} | ||||
|                                 </a> | ||||
|                             </li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|  | ||||
| @@ -123,6 +128,20 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         {% endfor %} | ||||
|                             <div class="tab-pane" id="search"> | ||||
|                                 <input class="form-control mx-auto d-block mb-3" | ||||
|                                        placeholder="{% trans "Search button..." %}" type="search" id="search-input"/> | ||||
|                                 <div class="d-inline-flex flex-wrap justify-content-center" id="search-results"> | ||||
|                                     {% for button in all_buttons %} | ||||
|                                         {% if button.display %} | ||||
|                                             <button class="btn btn-outline-dark rounded-0 flex-fill" hidden | ||||
|                                                     id="search_button{{ button.id }}" name="button" value="{{ button.name }}"> | ||||
|                                                 {{ button.name }} ({{ button.amount | pretty_money }}) | ||||
|                                             </button> | ||||
|                                         {% endif %} | ||||
|                                     {% endfor %} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
| @@ -163,7 +182,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     <script type="text/javascript"> | ||||
|         {% for button in highlighted %} | ||||
|             {% if button.display %} | ||||
|                 $("#highlighted_button{{ button.id }}").click(function() { | ||||
|                 document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() { | ||||
|                     addConso({{ button.destination_id }}, {{ button.amount }}, | ||||
|                         {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", | ||||
|                         {{ button.id }}, "{{ button.name|escapejs }}"); | ||||
| @@ -174,7 +193,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         {% for category in categories %} | ||||
|             {% for button in category.templates_filtered %} | ||||
|             {% if button.display %} | ||||
|                 $("#button{{ button.id }}").click(function() { | ||||
|                 document.getElementById("button{{ button.id }}").addEventListener("click", function() { | ||||
|                     addConso({{ button.destination_id }}, {{ button.amount }}, | ||||
|                         {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", | ||||
|                         {{ button.id }}, "{{ button.name|escapejs }}"); | ||||
| @@ -182,5 +201,15 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             {% endif %} | ||||
|             {% endfor %} | ||||
|         {% endfor %} | ||||
|  | ||||
|         {% for button in all_buttons %} | ||||
|             {% if button.display %} | ||||
|                 document.getElementById("search_button{{ button.id }}").addEventListener("click", function() { | ||||
|                     addConso({{ button.destination_id }}, {{ button.amount }}, | ||||
|                         {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", | ||||
|                         {{ button.id }}, "{{ button.name|escapejs }}"); | ||||
|                 }); | ||||
|             {% endif %} | ||||
|         {% endfor %} | ||||
|     </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -22,8 +22,8 @@ | ||||
| </p> | ||||
|  | ||||
| <p> | ||||
|     Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde | ||||
|     est inférieur à 0 € depuis plus de 24h. | ||||
|     Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde | ||||
|     est inférieur à 0 €. | ||||
| </p> | ||||
|  | ||||
| <p> | ||||
|   | ||||
| @@ -9,7 +9,7 @@ Ce mail t'a été envoyé parce que le solde de ta Note Kfet | ||||
|  | ||||
| Ton solde actuel est de {{ note.balance|pretty_money }}. | ||||
|  | ||||
| Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde | ||||
| Par ailleurs, le BDE ne sert pas d'alcool aux adhérent·e·s dont le solde | ||||
| est inférieur à 0 € depuis plus de 24h. | ||||
|  | ||||
| Si tu ne comprends pas ton solde, tu peux consulter ton historique | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django import template | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django import template | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from api.tests import TestAPI | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.urls import path | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import json | ||||
| @@ -10,12 +10,12 @@ from django.core.exceptions import PermissionDenied | ||||
| from django.db.models import Q, F | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views.generic import CreateView, UpdateView, DetailView | ||||
| from django_tables2 import SingleTableView | ||||
| from django.urls import reverse_lazy | ||||
| from django_tables2 import SingleTableView | ||||
| from activity.models import Entry | ||||
| from note_kfet.inputs import AmountInput | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
| from note_kfet.inputs import AmountInput | ||||
|  | ||||
| from .forms import TransactionTemplateForm, SearchTransactionForm | ||||
| from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note | ||||
| @@ -190,6 +190,10 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|         ).order_by('name').all() | ||||
|         context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk | ||||
|  | ||||
|         context['all_buttons'] = TransactionTemplate.objects.filter( | ||||
|             PermissionBackend.filter_queryset(self.request, TransactionTemplate, "view") | ||||
|         ).filter(display=True).order_by('name').all() | ||||
|  | ||||
|         return context | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'permission.apps.PermissionConfig' | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-lateré | ||||
|  | ||||
| from django.contrib import admin | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import PermissionViewSet, RoleViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from api.viewsets import ReadOnlyProtectedModelViewSet | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
| @@ -198,6 +198,41 @@ class PermissionBackend(ModelBackend): | ||||
|     def has_module_perms(self, user_obj, app_label): | ||||
|         return False | ||||
|  | ||||
|     @staticmethod | ||||
|     @memoize | ||||
|     def has_model_perm(request, model, type): | ||||
|         """ | ||||
|         Check is the given user has the permission over a given model for a given action. | ||||
|         The result is then memoized. | ||||
|         :param request: The current request | ||||
|         :param model: The model that the permissions shoud apply | ||||
|         :param type: The type of the permissions: view, change, add or delete | ||||
|         For view action, it is consider possible if user can view or change the model | ||||
|         """ | ||||
|         # Requested by a shell | ||||
|         if request is None: | ||||
|             return False | ||||
|  | ||||
|         user_obj = request.user | ||||
|         sess = request.session | ||||
|  | ||||
|         if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'): | ||||
|             # OAuth2 Authentication | ||||
|             user_obj = request.auth.user | ||||
|  | ||||
|         if user_obj is None or user_obj.is_anonymous: | ||||
|             return False | ||||
|  | ||||
|         if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42: | ||||
|             return True | ||||
|  | ||||
|         ct = ContentType.objects.get_for_model(model) | ||||
|         if any(PermissionBackend.permissions(request, ct, type)): | ||||
|             return True | ||||
|         if type == "view" and any(PermissionBackend.permissions(request, ct, "change")): | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def get_all_permissions(self, user_obj, obj=None): | ||||
|         ct = ContentType.objects.get_for_model(obj) | ||||
|         return list(self.permissions(get_current_request(), ct, "view")) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| import sys | ||||
| from functools import lru_cache | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										19
									
								
								apps/permission/migrations/0002_club_not_required.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/permission/migrations/0002_club_not_required.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 2.2.28 on 2023-07-24 10:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('permission', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='role', | ||||
|             name='for_club', | ||||
|             field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import functools | ||||
| @@ -26,6 +26,15 @@ class InstancedPermission: | ||||
|         self.mask = mask | ||||
|         self.kwargs = kwargs | ||||
|  | ||||
|     def __repr__(self): | ||||
|         if self.field: | ||||
|             return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) | ||||
|         else: | ||||
|             return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.__repr__() | ||||
|  | ||||
|     def applies(self, obj, permission_type, field_name=None): | ||||
|         """ | ||||
|         Returns True if the permission applies to | ||||
| @@ -84,21 +93,11 @@ class InstancedPermission: | ||||
|             # noinspection PyProtectedMember | ||||
|             self.query = Permission._about(self.raw_query, **self.kwargs) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         if self.field: | ||||
|             return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) | ||||
|         else: | ||||
|             return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.__repr__() | ||||
|  | ||||
|  | ||||
| class PermissionMask(models.Model): | ||||
|     """ | ||||
|     Permissions that are hidden behind a mask | ||||
|     """ | ||||
|  | ||||
|     rank = models.PositiveSmallIntegerField( | ||||
|         unique=True, | ||||
|         verbose_name=_('rank'), | ||||
| @@ -110,13 +109,13 @@ class PermissionMask(models.Model): | ||||
|         verbose_name=_('description'), | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("permission mask") | ||||
|         verbose_name_plural = _("permission masks") | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|  | ||||
| class Permission(models.Model): | ||||
|  | ||||
| @@ -194,16 +193,19 @@ class Permission(models.Model): | ||||
|         verbose_name = _("permission") | ||||
|         verbose_name_plural = _("permissions") | ||||
|  | ||||
|     def clean(self): | ||||
|         self.query = json.dumps(json.loads(self.query)) | ||||
|         if self.field and self.type not in {'view', 'change'}: | ||||
|             raise ValidationError(_("Specifying field applies only to view and change permission types.")) | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, **kwargs): | ||||
|         self.full_clean() | ||||
|         super().save() | ||||
|  | ||||
|     def clean(self): | ||||
|         self.query = json.dumps(json.loads(self.query)) | ||||
|         if self.field and self.type not in {'view', 'change'}: | ||||
|             raise ValidationError(_("Specifying field applies only to view and change permission types.")) | ||||
|  | ||||
|     @staticmethod | ||||
|     def compute_f(oper, **kwargs): | ||||
|         if isinstance(oper, list): | ||||
| @@ -317,9 +319,6 @@ class Permission(models.Model): | ||||
|         # query = self._about(query, **kwargs) | ||||
|         return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|  | ||||
| class Role(models.Model): | ||||
|     """ | ||||
| @@ -339,13 +338,14 @@ class Role(models.Model): | ||||
|         "member.Club", | ||||
|         verbose_name=_("for club"), | ||||
|         on_delete=models.PROTECT, | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         default=None, | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("role permissions") | ||||
|         verbose_name_plural = _("role permissions") | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework.permissions import DjangoObjectPermissions | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| from oauth2_provider.oauth2_validators import OAuth2Validator | ||||
| from oauth2_provider.scopes import BaseScopes | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.core.exceptions import PermissionDenied | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import django_tables2 as tables | ||||
| @@ -36,8 +36,8 @@ class RightsTable(tables.Table): | ||||
|  | ||||
|     def render_roles(self, record): | ||||
|         # If the user has the right to manage the roles, display the link to manage them | ||||
|         roles = record.roles.filter((~(Q(name="Adhérent BDE") | ||||
|                                      | Q(name="Adhérent Kfet") | ||||
|         roles = record.roles.filter((~(Q(name="Adhérent⋅e BDE") | ||||
|                                      | Q(name="Adhérent⋅e Kfet") | ||||
|                                      | Q(name="Membre de club") | ||||
|                                      | Q(name="Bureau de club")) | ||||
|                                      & Q(weirole__isnull=True))).all() | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import timedelta | ||||
| @@ -58,7 +58,7 @@ class OAuth2TestCase(TestCase): | ||||
|         # Create membership to validate permissions | ||||
|         NoteUser.objects.create(user=self.user) | ||||
|         membership = Membership.objects.create(user=self.user, club_id=bde.pk) | ||||
|         membership.roles.add(Role.objects.get(name="Adhérent BDE")) | ||||
|         membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE")) | ||||
|         membership.save() | ||||
|  | ||||
|         # User is now a member and can now see its own user detail | ||||
| @@ -85,7 +85,7 @@ class OAuth2TestCase(TestCase): | ||||
|         bde = Club.objects.get(name="BDE") | ||||
|         NoteUser.objects.create(user=self.user) | ||||
|         membership = Membership.objects.create(user=self.user, club_id=bde.pk) | ||||
|         membership.roles.add(Role.objects.get(name="Adhérent BDE")) | ||||
|         membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE")) | ||||
|         membership.save() | ||||
|  | ||||
|         resp = self.client.get(reverse('permission:scopes')) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import timedelta, date | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.auth.models import User | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # Copyright (C) 2018-2021 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user