mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	Compare commits
	
		
			189 Commits
		
	
	
		
			inclusive
			...
			traduction
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					354c79bb82 | ||
| 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 | ||
| 
						 | 
					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 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -42,6 +42,7 @@ map.json
 | 
			
		||||
backups/
 | 
			
		||||
/static/
 | 
			
		||||
/media/
 | 
			
		||||
/tmp/
 | 
			
		||||
 | 
			
		||||
# Virtualenv
 | 
			
		||||
env/
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -17,4 +17,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% 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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
    </h3>
 | 
			
		||||
    {% render_table table %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,29 @@ 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()
 | 
			
		||||
 | 
			
		||||
        while (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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Membership(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
@@ -333,6 +344,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 +473,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 +518,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,13 +20,13 @@ 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
 | 
			
		||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
 | 
			
		||||
 | 
			
		||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\
 | 
			
		||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
 | 
			
		||||
    CustomAuthenticationForm, MembershipRolesForm
 | 
			
		||||
from .models import Club, Membership
 | 
			
		||||
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
 | 
			
		||||
@@ -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,12 +326,15 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
 | 
			
		||||
        """Save image to note"""
 | 
			
		||||
        image = form.cleaned_data['image']
 | 
			
		||||
 | 
			
		||||
        # Rename as a PNG or GIF
 | 
			
		||||
        extension = image.name.split(".")[-1]
 | 
			
		||||
        if extension == "gif":
 | 
			
		||||
            image.name = "{}_pic.gif".format(self.object.note.pk)
 | 
			
		||||
        if image is None:
 | 
			
		||||
            image = "pic/default.png"
 | 
			
		||||
        else:
 | 
			
		||||
            image.name = "{}_pic.png".format(self.object.note.pk)
 | 
			
		||||
            # Rename as a PNG or GIF
 | 
			
		||||
            extension = image.name.split(".")[-1]
 | 
			
		||||
            if extension == "gif":
 | 
			
		||||
                image.name = "{}_pic.gif".format(self.object.note.pk)
 | 
			
		||||
            else:
 | 
			
		||||
                image.name = "{}_pic.png".format(self.object.note.pk)
 | 
			
		||||
 | 
			
		||||
        # Save
 | 
			
		||||
        self.object.note.display_image = image
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +13,7 @@ from rest_framework import status
 | 
			
		||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
 | 
			
		||||
from permission.backends import PermissionBackend
 | 
			
		||||
 | 
			
		||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
 | 
			
		||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
 | 
			
		||||
    TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
 | 
			
		||||
    TrustSerializer
 | 
			
		||||
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, 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 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>
 | 
			
		||||
@@ -43,4 +43,4 @@
 | 
			
		||||
    {% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
 | 
			
		||||
</p>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -22,4 +22,4 @@ virement bancaire.
 | 
			
		||||
--
 | 
			
		||||
Le BDE
 | 
			
		||||
 | 
			
		||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
 | 
			
		||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -10,7 +10,7 @@ from django.urls import reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from permission.models import Role
 | 
			
		||||
 | 
			
		||||
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
 | 
			
		||||
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet, \
 | 
			
		||||
    TransactionTemplateViewSet, TransactionViewSet
 | 
			
		||||
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
 | 
			
		||||
    MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user