mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-30 23:39:54 +01:00 
			
		
		
		
	Compare commits
	
		
			164 Commits
		
	
	
		
			v1.0.0
			...
			b4a1b513cc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | b4a1b513cc | ||
| c0c64f225c | |||
| c8f7986d5a | |||
|  | d3a9c442a5 | ||
|  | 016ab5a9c9 | ||
|  | 7866ab7ec0 | ||
|  | f570ff3cd5 | ||
|  | 5cb4183e9f | ||
|  | 3a20555663 | ||
|  | 95be0042e9 | ||
|  | 48880e7fd3 | ||
|  | e0030771e4 | ||
|  | d47799e6ee | ||
|  | eae091625a | ||
|  | aceb77ffb9 | ||
|  | 338c94ed05 | ||
|  | 290848f904 | ||
|  | 296b94d237 | ||
|  | 4942553335 | ||
|  | c1efb87180 | ||
|  | 72eead8595 | ||
|  | ade7e583e5 | ||
| 4a8a101822 | |||
| dd2cfa6327 | |||
| 2adf84b7fc | |||
|  | 2f54e64ea2 | ||
|  | 8434c0062c | ||
|  | 6d976f32bf | ||
|  | b9d49d53f2 | ||
|  | 23243e09bb | ||
|  | 2682e9a610 | ||
|  | 5635598bbc | ||
|  | b58a0c43cd | ||
|  | e1f647bd02 | ||
|  | 39fd3a2471 | ||
|  | 1072e227b8 | ||
|  | cbf7e6fe6c | ||
|  | 950922d041 | ||
|  | 78fe070cd3 | ||
|  | 51d5733578 | ||
|  | 7bd895c1df | ||
|  | e5e94c52f2 | ||
|  | 051591cb7a | ||
|  | 0e7390b669 | ||
|  | fe4363b83d | ||
|  | 6e80016b38 | ||
|  | 08e50ffc22 | ||
|  | 9cb65277f3 | ||
|  | 224a0fdd8c | ||
|  | 6dc7604e90 | ||
|  | cb7f3c9f18 | ||
|  | f910feca9e | ||
|  | 91f784872c | ||
|  | b655135a42 | ||
|  | 58aa4983e3 | ||
|  | 6cc3cf4174 | ||
|  | 2097e67321 | ||
|  | d773303d18 | ||
|  | 3cabcf40e7 | ||
|  | bf29efda0a | ||
|  | ceccba0d71 | ||
|  | 3eced33082 | ||
|  | acb3fb4a91 | ||
|  | 1c5e951c2f | ||
|  | beb1853aef | ||
|  | 0078eb8f90 | ||
|  | e5e758f9d9 | ||
|  | 4a78328717 | ||
|  | 65a2e8c08c | ||
|  | b5fa428bad | ||
|  | fb72385773 | ||
|  | 2f68601e8b | ||
|  | 0b1bed8048 | ||
|  | 8ada0e51f2 | ||
|  | c3d613947f | ||
|  | 36b8157372 | ||
|  | 992cfe8e23 | ||
|  | 18a8ff1b8a | ||
|  | c61bb2e90d | ||
|  | 4b12e3ed08 | ||
|  | af07ed9807 | ||
|  | bbe53b3b63 | ||
|  | 536f0ec226 | ||
|  | 541ed59f40 | ||
|  | e172b4f4bb | ||
|  | d666179037 | ||
|  | f22e92132c | ||
|  | ca7ad05746 | ||
|  | f55ca2f725 | ||
|  | d4e4ed580f | ||
|  | 8756751344 | ||
|  | fd83fe19bf | ||
|  | a00d95608b | ||
|  | 3303edd01f | ||
|  | e48ef92137 | ||
|  | 919d0b7e85 | ||
|  | 439bf35b62 | ||
|  | 74b26335d1 | ||
|  | 3d733ed6af | ||
|  | d54ab94ceb | ||
|  | 4f188ca3e5 | ||
|  | 72bac75fbd | ||
|  | 6d54aae614 | ||
|  | 8052152ea5 | ||
|  | 70448db8e5 | ||
|  | ac2d1e8111 | ||
|  | 3ba61385a3 | ||
|  | 7353348d7a | ||
|  | f63e2e088e | ||
|  | 420a24ebac | ||
|  | d566def706 | ||
|  | eaf6769e8b | ||
|  | a61ec81cff | ||
|  | 60f2a73cc5 | ||
|  | bcd96b2ed8 | ||
|  | 5c702187e5 | ||
|  | 905d65371f | ||
|  | 180cd3e1ec | ||
|  | 73ca65aa91 | ||
|  | 5ed0560953 | ||
|  | dbc6fbbf71 | ||
|  | 872fd8f86d | ||
|  | f89234b69a | ||
|  | 36a980555b | ||
|  | 826cd4d87f | ||
|  | e8005a6c58 | ||
|  | 2270a0aa82 | ||
|  | 0f53ac45f7 | ||
|  | 670556c59e | ||
|  | 5b02ba48e0 | ||
|  | f3f18bc25e | ||
|  | 03124e124c | ||
|  | 6308964e93 | ||
|  | ed79097288 | ||
|  | d7eaef8cee | ||
|  | 01d405e54b | ||
|  | 80e3cba4c6 | ||
|  | f190053e84 | ||
|  | 218960adb5 | ||
|  | 88a1eae631 | ||
|  | 2a2ecb2acc | ||
|  | f5486bdb63 | ||
|  | 9b090a145c | ||
|  | 860c7b50e5 | ||
|  | afdc75c0bd | ||
|  | c6603e8aa7 | ||
|  | 72cc1638e6 | ||
|  | 6a0dc4cb10 | ||
|  | 0f1f3b9560 | ||
|  | c720e5483e | ||
|  | 0fd3e9db78 | ||
|  | c34296c923 | ||
|  | ce4c22a4a1 | ||
|  | 3e0f665ef8 | ||
|  | be8751c815 | ||
|  | 8225445c3e | ||
|  | f333e6a875 | ||
|  | e5835b46a5 | ||
|  | fe937405a6 | ||
|  | 0741c8ad2b | ||
|  | 3191dba31f | ||
|  | 428de69d93 | ||
|  | 0888afe439 | ||
|  | 3111c30e56 | 
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -47,3 +47,8 @@ backups/ | ||||
| env/ | ||||
| venv/ | ||||
| db.sqlite3 | ||||
|  | ||||
| # ansibles customs host | ||||
| ansible/host_vars/*.yaml | ||||
| !ansible/host_vars/bde* | ||||
| ansible/hosts | ||||
|   | ||||
| @@ -16,8 +16,8 @@ py37-django22: | ||||
|         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-cas-server python3-psycopg2 python3-pil | ||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers | ||||
|         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 | ||||
|  | ||||
| @@ -33,11 +33,26 @@ py38-django22: | ||||
|         apt-get install --no-install-recommends -y | ||||
|         python3-django python3-django-crispy-forms | ||||
|         python3-django-extensions python3-django-filters python3-django-polymorphic | ||||
|         python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil | ||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers | ||||
|         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 py38-django22 | ||||
|  | ||||
| # Debian Bullseye | ||||
| py39-django22: | ||||
|   stage: test | ||||
|   image: debian:bullseye | ||||
|   before_script: | ||||
|     - > | ||||
|         apt-get update && | ||||
|         apt-get install --no-install-recommends -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 py39-django22 | ||||
|  | ||||
| linters: | ||||
|   stage: quality-assurance | ||||
|   image: debian:buster-backports | ||||
|   | ||||
| @@ -8,8 +8,8 @@ RUN 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-cas-server python3-psycopg2 python3-pil \ | ||||
|     python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ | ||||
|     python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \ | ||||
|     python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \ | ||||
|     python3-bs4 python3-setuptools \ | ||||
|     uwsgi uwsgi-plugin-python3 \ | ||||
|     texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \ | ||||
|   | ||||
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
								
							| @@ -69,13 +69,31 @@ accessible depuis l'ensemble de votre réseau, pratique pour tester le rendu | ||||
| de la note sur un téléphone ! | ||||
|  | ||||
| ## Installation d'une instance de production | ||||
| Pour déployer facilement la note il est possible d'utiliser le playbook Ansible (sinon vous pouvez toujours le faire a la main, voir plus bas). | ||||
| ### Avec ansible | ||||
| Il vous faudra un serveur sous debian ou ubuntu connecté à internet et que vous souhaiterez accéder à cette instance de la note sur `note.nomdedomaine.tld`. | ||||
|  | ||||
| 0. Installer Ansible sur votre machine personnelle. | ||||
|  | ||||
| 0. (bis) cloner le dépot sur votre machine personelle. | ||||
|  | ||||
| 1.  Copier le fichier `ansible/host_example` | ||||
| ``` bash | ||||
| $ cp ansible/hosts_example ansible/hosts | ||||
| ``` | ||||
| et ajouter sous [dev] et/ou [prod] les serveurs sur lesquels vous souhaitez installer la note. | ||||
| 2.  Créer un fichier `ansible/host_vars/<note.nomdedomaine.tld.yaml>` sur le modèle des fichiers existants dans `ansible/hosts` et compléter les variables nécessaires. | ||||
|  | ||||
| 3. lancer `ansible/base.yaml -l <nomdedomaine.tld.yaml>` | ||||
| 4. Aller vous faire un café, ca peux durer un moment. | ||||
|  | ||||
| ### Installation manuelle | ||||
|  | ||||
| **En production on souhaite absolument utiliser les modules Python packagées dans le gestionnaire de paquet.** | ||||
| Cela permet de mettre à jour facilement les dépendances critiques telles que Django. | ||||
|  | ||||
| L'installation d'une instance de production néccessite **une installation de Debian Buster ou d'Ubuntu 20.04**. | ||||
|  | ||||
| Pour aller vite vous pouvez lancer le Playbook Ansible fournit dans ce dépôt en l'adaptant. | ||||
| Sinon vous pouvez suivre les étapes décrites ci-dessous. | ||||
|  | ||||
| 0.  Sous Debian Buster, **activer Debian Backports.** En effet Django 2.2 LTS n'est que disponible dans les backports. | ||||
| @@ -93,10 +111,10 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. | ||||
|     $ sudo apt 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-cas-server python3-psycopg2 python3-pil \ | ||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ | ||||
|         python3-bs4 python3-setuptools \ | ||||
|         uwsgi uwsgi-plugin-python3 \ | ||||
|         python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \ | ||||
|         python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \ | ||||
|         python3-bs4 python3-setuptools python3-docutils \ | ||||
|         memcached uwsgi uwsgi-plugin-python3 \ | ||||
|         texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \ | ||||
|         nginx python3-venv git acl | ||||
|     ``` | ||||
| @@ -267,14 +285,18 @@ La documentation plus haut niveau sur le développement est disponible sur [le W | ||||
|  | ||||
| ### Regénérer les fichiers de traduction | ||||
|  | ||||
| Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv. | ||||
| Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. | ||||
| Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv. | ||||
| De plus, il faut aussi extraire les variables des fichiers JavaScript. | ||||
|  | ||||
| ```bash | ||||
| django-admin makemessages -i env | ||||
| python3 manage.py makemessages -i env | ||||
| python3 manage.py makemessages -i env -e js -d djangojs | ||||
| ``` | ||||
|  | ||||
| Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec | ||||
|  | ||||
| ```bash | ||||
| django-admin compilemessages | ||||
| python3 manage.py compilemessages | ||||
| python3 manage.py compilejsmessages | ||||
| ``` | ||||
|   | ||||
| @@ -3,3 +3,4 @@ note: | ||||
|   server_name: note-beta.crans.org | ||||
|   git_branch: beta | ||||
|   cron_enabled: false | ||||
|   email: notekfet2020@lists.crans.org | ||||
|   | ||||
| @@ -3,3 +3,4 @@ note: | ||||
|   server_name: note-dev.crans.org | ||||
|   git_branch: beta | ||||
|   cron_enabled: false | ||||
|   email: notekfet2020@lists.crans.org | ||||
| @@ -1,5 +1,5 @@ | ||||
| [dev] | ||||
| bde3-virt.adh.crans.org | ||||
| bde-note-dev.adh.crans.org | ||||
| bde-nk20-beta.adh.crans.org | ||||
| 
 | ||||
| [prod] | ||||
| @@ -3,11 +3,12 @@ | ||||
|   apt_repository: | ||||
|     repo: deb http://{{ mirror }}/debian buster-backports main | ||||
|     state: present | ||||
|   when: ansible_facts['distribution'] == "Debian" | ||||
|  | ||||
| - name: Install note_kfet APT dependencies | ||||
|   apt: | ||||
|     update_cache: true | ||||
|     default_release: buster-backports | ||||
|     default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}" | ||||
|     install_recommends: false | ||||
|     name: | ||||
|       # Common tools | ||||
| @@ -23,13 +24,14 @@ | ||||
|       - python3-babel | ||||
|       - python3-bs4 | ||||
|       - python3-django | ||||
|       - python3-django-cas-server | ||||
|       - python3-django-crispy-forms | ||||
|       - python3-django-extensions | ||||
|       - python3-django-filters | ||||
|       - python3-django-oauth-toolkit | ||||
|       - python3-django-polymorphic | ||||
|       - python3-djangorestframework | ||||
|       - python3-lockfile | ||||
|       - python3-memcache | ||||
|       - python3-phonenumbers | ||||
|       - python3-pil | ||||
|       - python3-pip | ||||
| @@ -40,6 +42,9 @@ | ||||
|       # LaTeX (PDF generation) | ||||
|       - texlive-xetex | ||||
|  | ||||
|       # Cache server | ||||
|       - memcached | ||||
|  | ||||
|       # WSGI server | ||||
|       - uwsgi | ||||
|       - uwsgi-plugin-python3 | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
| - name: Use default env vars (should be updated!) | ||||
|   template: | ||||
|     src: "env_example" | ||||
|     src: "env.j2" | ||||
|     dest: "/var/www/note_kfet/.env" | ||||
|     mode: 0644 | ||||
|     force: false | ||||
|   | ||||
							
								
								
									
										23
									
								
								ansible/roles/2-nk20/templates/env.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ansible/roles/2-nk20/templates/env.j2
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| DJANGO_APP_STAGE=prod | ||||
| # Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev | ||||
| DJANGO_DEV_STORE_METHOD=sqlite | ||||
| DJANGO_DB_HOST=localhost | ||||
| DJANGO_DB_NAME=note_db | ||||
| DJANGO_DB_USER=note | ||||
| DJANGO_DB_PASSWORD={{ DB_PASSWORD }} | ||||
| DJANGO_DB_PORT= | ||||
| DJANGO_SECRET_KEY=CHANGE_ME | ||||
| DJANGO_SETTINGS_MODULE=note_kfet.settings | ||||
| CONTACT_EMAIL=tresorerie.bde@localhost | ||||
| NOTE_URL= {{note.server_name}} | ||||
|  | ||||
| # Config for mails. Only used in production | ||||
| NOTE_MAIL=notekfet@localhost | ||||
| EMAIL_HOST=smtp.localhost | ||||
| EMAIL_PORT=25 | ||||
| EMAIL_USER=notekfet@localhost | ||||
| EMAIL_PASSWORD=CHANGE_ME | ||||
|  | ||||
| # Wiki configuration | ||||
| WIKI_USER=NoteKfet2020 | ||||
| WIKI_PASSWORD= | ||||
| @@ -9,6 +9,11 @@ | ||||
|   retries: 3 | ||||
|   until: pkg_result is succeeded | ||||
|  | ||||
| - name: Check if certificate already exists. | ||||
|   stat: | ||||
|     path: /etc/letsencrypt/live/{{note.server_name}}/cert.pem | ||||
|   register: letsencrypt_cert | ||||
|  | ||||
| - name: Create /etc/letsencrypt/conf.d | ||||
|   file: | ||||
|     path: /etc/letsencrypt/conf.d | ||||
| @@ -19,3 +24,17 @@ | ||||
|     src: "letsencrypt/conf.d/nk20.ini.j2" | ||||
|     dest: "/etc/letsencrypt/conf.d/nk20.ini" | ||||
|     mode: 0644 | ||||
|  | ||||
| - name: Stop services to allow certbot to generate a cert. | ||||
|   service: | ||||
|     name: nginx | ||||
|     state: stopped | ||||
|  | ||||
| - name: Generate new certificate if one doesn't exist. | ||||
|   shell: "certbot certonly --non-interactive --config /etc/letsencrypt/conf.d/nk20.ini -d {{note.server_name}}" | ||||
|   when: letsencrypt_cert.stat.exists == False | ||||
|  | ||||
| - name: Restart services to allow certbot to generate a cert. | ||||
|   service: | ||||
|     name: nginx | ||||
|     state: started | ||||
|   | ||||
| @@ -10,11 +10,11 @@ rsa-key-size = 4096 | ||||
| # server = https://acme-staging.api.letsencrypt.org/directory | ||||
|  | ||||
| # Uncomment and update to register with the specified e-mail address | ||||
| email = notekfet2020@lists.crans.org | ||||
| email = {{ note.email }} | ||||
|  | ||||
| # Uncomment to use a text interface instead of ncurses | ||||
| text = True | ||||
|  | ||||
| # Use DNS-01 challenge | ||||
| authenticator = nginx | ||||
| authenticator = standalone | ||||
|  | ||||
|   | ||||
| @@ -11,14 +11,14 @@ | ||||
|   until: pkg_result is succeeded | ||||
|  | ||||
| - name: Create role note | ||||
|   when: "DB_PASSWORD|bool"    # If the password is not defined, skip the installation | ||||
|   when: DB_PASSWORD|length > 0 # If the password is not defined, skip the installation | ||||
|   postgresql_user: | ||||
|     name: note | ||||
|     password: "{{ DB_PASSWORD }}" | ||||
|   become_user: postgres | ||||
|  | ||||
| - name: Create NK20 database | ||||
|   when: "DB_PASSWORD|bool" | ||||
|   when: DB_PASSWORD|length >0 | ||||
|   postgresql_db: | ||||
|     name: note_db | ||||
|     owner: note | ||||
|   | ||||
| @@ -1,4 +1,10 @@ | ||||
| --- | ||||
| - name: Collect static files | ||||
|   command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput | ||||
|   args: | ||||
|     chdir: /var/www/note_kfet | ||||
|   become_user: www-data | ||||
|  | ||||
| - name: Migrate Django database | ||||
|   command: /var/www/note_kfet/env/bin/python manage.py migrate | ||||
|   args: | ||||
| @@ -11,14 +17,14 @@ | ||||
|     chdir: /var/www/note_kfet | ||||
|   become_user: www-data | ||||
|  | ||||
| - name: Compile JavaScript messages | ||||
|   command: /var/www/note_kfet/env/bin/python manage.py compilejsmessages | ||||
|   args: | ||||
|     chdir: /var/www/note_kfet | ||||
|   become_user: www-data | ||||
|  | ||||
| - name: Install initial fixtures | ||||
|   command: /var/www/note_kfet/env/bin/python manage.py loaddata initial | ||||
|   args: | ||||
|     chdir: /var/www/note_kfet | ||||
|   become_user: postgres | ||||
|  | ||||
| - name: Collect static files | ||||
|   command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput | ||||
|   args: | ||||
|     chdir: /var/www/note_kfet | ||||
|   become_user: www-data | ||||
|   | ||||
| @@ -15,10 +15,10 @@ class ActivityTypeViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/type/ | ||||
|     """ | ||||
|     queryset = ActivityType.objects.all() | ||||
|     queryset = ActivityType.objects.order_by('id') | ||||
|     serializer_class = ActivityTypeSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['name', 'can_invite', ] | ||||
|     filterset_fields = ['name', 'manage_entries', 'can_invite', 'guest_entry_fee', ] | ||||
|  | ||||
|  | ||||
| class ActivityViewSet(ReadProtectedModelViewSet): | ||||
| @@ -27,10 +27,16 @@ class ActivityViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/activity/ | ||||
|     """ | ||||
|     queryset = Activity.objects.all() | ||||
|     queryset = Activity.objects.order_by('id') | ||||
|     serializer_class = ActivitySerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['name', 'description', 'activity_type', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club', | ||||
|                         'date_start', 'date_end', 'valid', 'open', ] | ||||
|     search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name', | ||||
|                      '$creater__email', '$creater__note__alias__name', '$creater__note__alias__normalized_name', | ||||
|                      '$organizer__name', '$organizer__email', '$organizer__note__alias__name', | ||||
|                      '$organizer__note__alias__normalized_name', '$attendees_club__name', '$attendees_club__email', | ||||
|                      '$attendees_club__note__alias__name', '$attendees_club__note__alias__normalized_name', ] | ||||
|  | ||||
|  | ||||
| class GuestViewSet(ReadProtectedModelViewSet): | ||||
| @@ -39,10 +45,13 @@ class GuestViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/guest/ | ||||
|     """ | ||||
|     queryset = Guest.objects.all() | ||||
|     queryset = Guest.objects.order_by('id') | ||||
|     serializer_class = GuestSerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name', | ||||
|                         'inviter__alias__normalized_name', ] | ||||
|     search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name', | ||||
|                      '$inviter__alias__normalized_name', ] | ||||
|  | ||||
|  | ||||
| class EntryViewSet(ReadProtectedModelViewSet): | ||||
| @@ -51,7 +60,9 @@ class EntryViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/entry/ | ||||
|     """ | ||||
|     queryset = Entry.objects.all() | ||||
|     queryset = Entry.objects.order_by('id') | ||||
|     serializer_class = EntrySerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['activity', 'time', 'note', 'guest', ] | ||||
|     search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name', | ||||
|                      '$guest__last_name', '$guest__first_name', ] | ||||
|   | ||||
| @@ -7,7 +7,7 @@ from threading import Thread | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.db import models | ||||
| from django.db import models, transaction | ||||
| from django.db.models import Q | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| @@ -123,6 +123,7 @@ class Activity(models.Model): | ||||
|         verbose_name=_('open'), | ||||
|     ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         Update the activity wiki page each time the activity is updated (validation, change description, ...) | ||||
| @@ -194,8 +195,8 @@ class Entry(models.Model): | ||||
|             else _("Entry for {note} to the activity {activity}").format( | ||||
|             guest=str(self.guest), note=str(self.note), activity=str(self.activity)) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|  | ||||
|         qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) | ||||
|         if qs.exists(): | ||||
|             raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) | ||||
| @@ -260,6 +261,7 @@ class Guest(models.Model): | ||||
|         except AttributeError: | ||||
|             return False | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, force_insert=False, force_update=False, using=None, update_fields=None): | ||||
|         one_year = timedelta(days=365) | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|          headers: {"X-CSRFTOKEN": CSRF_TOKEN} | ||||
|      }) | ||||
|       .done(function() { | ||||
|           addMsg('Invité supprimé','success'); | ||||
|           addMsg('{% trans "Guest deleted" %}', 'success'); | ||||
|           $("#guests_table").load(location.pathname + " #guests_table"); | ||||
|       }) | ||||
|       .fail(function(xhr, textStatus, error) { | ||||
|   | ||||
| @@ -86,10 +86,10 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                 }).done(function () { | ||||
|                     if (target.hasClass("table-info")) | ||||
|                         addMsg( | ||||
|                             "Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", | ||||
|                             "{% trans "Entry done, but caution: the user is not a Kfet member." %}", | ||||
|                             "warning", 10000); | ||||
|                     else | ||||
|                         addMsg("Entrée effectuée !", "success", 4000); | ||||
|                         addMsg("Entry made!", "success", 4000); | ||||
|                     reloadTable(true); | ||||
|                 }).fail(function (xhr) { | ||||
|                     errMsg(xhr.responseJSON, 4000); | ||||
| @@ -121,10 +121,10 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                     }).done(function () { | ||||
|                         if (target.hasClass("table-info")) | ||||
|                             addMsg( | ||||
|                                 "Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", | ||||
|                                 "{% trans "Entry done, but caution: the user is not a Kfet member." %}", | ||||
|                                 "warning", 10000); | ||||
|                         else | ||||
|                             addMsg("Entrée effectuée !", "success", 4000); | ||||
|                             addMsg("{% trans "Entry done!" %}", "success", 4000); | ||||
|                         reloadTable(true); | ||||
|                     }).fail(function (xhr) { | ||||
|                         errMsg(xhr.responseJSON, 4000); | ||||
|   | ||||
| @@ -3,13 +3,16 @@ | ||||
|  | ||||
| from datetime import timedelta | ||||
|  | ||||
| from api.tests import TestAPI | ||||
| from django.contrib.auth.models import User | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
| from django.utils import timezone | ||||
| from activity.models import Activity, ActivityType, Guest, Entry | ||||
| from member.models import Club | ||||
|  | ||||
| from ..api.views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet | ||||
| from ..models import Activity, ActivityType, Guest, Entry | ||||
|  | ||||
|  | ||||
| class TestActivities(TestCase): | ||||
|     """ | ||||
| @@ -173,3 +176,58 @@ class TestActivities(TestCase): | ||||
|         """ | ||||
|         response = self.client.get(reverse("activity:calendar_ics")) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|  | ||||
| class TestActivityAPI(TestAPI): | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|  | ||||
|         self.activity = Activity.objects.create( | ||||
|             name="Activity", | ||||
|             description="This is a test activity\non two very very long lines\nbecause this is very important.", | ||||
|             location="Earth", | ||||
|             activity_type=ActivityType.objects.get(name="Pot"), | ||||
|             creater=self.user, | ||||
|             organizer=Club.objects.get(name="Kfet"), | ||||
|             attendees_club=Club.objects.get(name="Kfet"), | ||||
|             date_start=timezone.now(), | ||||
|             date_end=timezone.now() + timedelta(days=2), | ||||
|             valid=True, | ||||
|         ) | ||||
|  | ||||
|         self.guest = Guest.objects.create( | ||||
|             activity=self.activity, | ||||
|             inviter=self.user.note, | ||||
|             last_name="GUEST", | ||||
|             first_name="Guest", | ||||
|         ) | ||||
|  | ||||
|         self.entry = Entry.objects.create( | ||||
|             activity=self.activity, | ||||
|             note=self.user.note, | ||||
|             guest=self.guest, | ||||
|         ) | ||||
|  | ||||
|     def test_activity_api(self): | ||||
|         """ | ||||
|         Load Activity API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ActivityViewSet, "/api/activity/activity/") | ||||
|  | ||||
|     def test_activity_type_api(self): | ||||
|         """ | ||||
|         Load ActivityType API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ActivityTypeViewSet, "/api/activity/type/") | ||||
|  | ||||
|     def test_entry_api(self): | ||||
|         """ | ||||
|         Load Entry API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(EntryViewSet, "/api/activity/entry/") | ||||
|  | ||||
|     def test_guest_api(self): | ||||
|         """ | ||||
|         Load Guest API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(GuestViewSet, "/api/activity/guest/") | ||||
|   | ||||
| @@ -7,12 +7,15 @@ from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.db import transaction | ||||
| from django.db.models import F, Q | ||||
| from django.http import HttpResponse | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils import timezone | ||||
| from django.utils.decorators import method_decorator | ||||
| 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 note.models import Alias, NoteSpecial, NoteUser | ||||
| @@ -44,6 +47,7 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             date_end=timezone.now(), | ||||
|         ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         form.instance.creater = self.request.user | ||||
|         return super().form_valid(form) | ||||
| @@ -145,6 +149,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         form.fields["inviter"].initial = self.request.user.note | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         form.instance.activity = Activity.objects\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"]) | ||||
| @@ -285,6 +290,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): | ||||
|         return context | ||||
|  | ||||
|  | ||||
| # Cache for 1 hour | ||||
| @method_decorator(cache_page(60 * 60), name='dispatch') | ||||
| class CalendarView(View): | ||||
|     """ | ||||
|     Render an ICS calendar with all valid activities. | ||||
|   | ||||
							
								
								
									
										237
									
								
								apps/api/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								apps/api/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import json | ||||
| from datetime import datetime, date | ||||
| from urllib.parse import quote_plus | ||||
| from warnings import warn | ||||
|  | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.db.models.fields.files import ImageFieldFile | ||||
| from django.test import TestCase | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from member.models import Membership, Club | ||||
| from note.models import NoteClub, NoteUser, Alias, Note | ||||
| from permission.models import PermissionMask, Permission, Role | ||||
| from phonenumbers import PhoneNumber | ||||
| from rest_framework.filters import SearchFilter, OrderingFilter | ||||
|  | ||||
| from .viewsets import ContentTypeViewSet, UserViewSet | ||||
|  | ||||
|  | ||||
| class TestAPI(TestCase): | ||||
|     """ | ||||
|     Load API pages and check that filters are working. | ||||
|     """ | ||||
|     fixtures = ('initial', ) | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.user = User.objects.create_superuser( | ||||
|             username="adminapi", | ||||
|             password="adminapi", | ||||
|             email="adminapi@example.com", | ||||
|             last_name="Admin", | ||||
|             first_name="Admin", | ||||
|         ) | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|         sess = self.client.session | ||||
|         sess["permission_mask"] = 42 | ||||
|         sess.save() | ||||
|  | ||||
|     def check_viewset(self, viewset, url): | ||||
|         """ | ||||
|         This function should be called inside a unit test. | ||||
|         This loads the viewset and for each filter entry, it checks that the filter is running good. | ||||
|         """ | ||||
|         resp = self.client.get(url + "?format=json") | ||||
|         self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|         model = viewset.serializer_class.Meta.model | ||||
|  | ||||
|         if not model.objects.exists():  # pragma: no cover | ||||
|             warn(f"Warning: unable to test API filters for the model {model._meta.verbose_name} " | ||||
|                  "since there is no instance of it.") | ||||
|             return | ||||
|  | ||||
|         if hasattr(viewset, "filter_backends"): | ||||
|             backends = viewset.filter_backends | ||||
|             obj = model.objects.last() | ||||
|  | ||||
|             if DjangoFilterBackend in backends: | ||||
|                 # Specific search | ||||
|                 for field in viewset.filterset_fields: | ||||
|                     obj = self.fix_note_object(obj, field) | ||||
|  | ||||
|                     value = self.get_value(obj, field) | ||||
|                     if value is None:  # pragma: no cover | ||||
|                         warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} " | ||||
|                              "has not been tested.") | ||||
|                         continue | ||||
|                     resp = self.client.get(url + f"?format=json&{field}={quote_plus(str(value))}") | ||||
|                     self.assertEqual(resp.status_code, 200, f"The filter {field} for the model " | ||||
|                                                             f"{model._meta.verbose_name} does not work. " | ||||
|                                                             f"Given parameter: {value}") | ||||
|                     content = json.loads(resp.content) | ||||
|                     self.assertGreater(content["count"], 0, f"The filter {field} for the model " | ||||
|                                                             f"{model._meta.verbose_name} does not work. " | ||||
|                                                             f"Given parameter: {value}") | ||||
|  | ||||
|             if OrderingFilter in backends: | ||||
|                 # Ensure that ordering is working well | ||||
|                 for field in viewset.ordering_fields: | ||||
|                     resp = self.client.get(url + f"?ordering={field}") | ||||
|                     self.assertEqual(resp.status_code, 200) | ||||
|                     resp = self.client.get(url + f"?ordering=-{field}") | ||||
|                     self.assertEqual(resp.status_code, 200) | ||||
|  | ||||
|             if SearchFilter in backends: | ||||
|                 # Basic search | ||||
|                 for field in viewset.search_fields: | ||||
|                     obj = self.fix_note_object(obj, field) | ||||
|  | ||||
|                     if field[0] == '$' or field[0] == '=': | ||||
|                         field = field[1:] | ||||
|                     value = self.get_value(obj, field) | ||||
|                     if value is None:  # pragma: no cover | ||||
|                         warn(f"Warning: the filter {field} for the model {model._meta.verbose_name} " | ||||
|                              "has not been tested.") | ||||
|                         continue | ||||
|                     resp = self.client.get(url + f"?format=json&search={quote_plus(str(value))}") | ||||
|                     self.assertEqual(resp.status_code, 200, f"The filter {field} for the model " | ||||
|                                                             f"{model._meta.verbose_name} does not work. " | ||||
|                                                             f"Given parameter: {value}") | ||||
|                     content = json.loads(resp.content) | ||||
|                     self.assertGreater(content["count"], 0, f"The filter {field} for the model " | ||||
|                                                             f"{model._meta.verbose_name} does not work. " | ||||
|                                                             f"Given parameter: {value}") | ||||
|  | ||||
|             self.check_permissions(url, obj) | ||||
|  | ||||
|     def check_permissions(self, url, obj): | ||||
|         """ | ||||
|         Check that permissions are working | ||||
|         """ | ||||
|         # Drop rights | ||||
|         self.user.is_superuser = False | ||||
|         self.user.save() | ||||
|         sess = self.client.session | ||||
|         sess["permission_mask"] = 0 | ||||
|         sess.save() | ||||
|  | ||||
|         # Delete user permissions | ||||
|         for m in Membership.objects.filter(user=self.user).all(): | ||||
|             m.roles.clear() | ||||
|             m.save() | ||||
|  | ||||
|         # Create a new role, which will have the checking permission | ||||
|         role = Role.objects.get_or_create(name="β-tester")[0] | ||||
|         role.permissions.clear() | ||||
|         role.save() | ||||
|         membership = Membership.objects.get_or_create(user=self.user, club=Club.objects.get(name="BDE"))[0] | ||||
|         membership.roles.set([role]) | ||||
|         membership.save() | ||||
|  | ||||
|         # Ensure that the access to the object is forbidden without permission | ||||
|         resp = self.client.get(url + f"{obj.pk}/") | ||||
|         self.assertEqual(resp.status_code, 404, f"Mysterious access to {url}{obj.pk}/ for {obj}") | ||||
|  | ||||
|         obj.refresh_from_db() | ||||
|  | ||||
|         # There are problems with polymorphism | ||||
|         if isinstance(obj, Note) and hasattr(obj, "note_ptr"): | ||||
|             obj = obj.note_ptr | ||||
|  | ||||
|         mask = PermissionMask.objects.get(rank=0) | ||||
|  | ||||
|         for field in obj._meta.fields: | ||||
|             # Build permission query | ||||
|             value = self.get_value(obj, field.name) | ||||
|             if isinstance(value, date) or isinstance(value, datetime): | ||||
|                 value = value.isoformat() | ||||
|             elif isinstance(value, ImageFieldFile): | ||||
|                 value = value.name | ||||
|             query = json.dumps({field.name: value}) | ||||
|  | ||||
|             # Create sample permission | ||||
|             permission = Permission.objects.get_or_create( | ||||
|                 model=ContentType.objects.get_for_model(obj._meta.model), | ||||
|                 query=query, | ||||
|                 mask=mask, | ||||
|                 type="view", | ||||
|                 permanent=False, | ||||
|                 description=f"Can view {obj._meta.verbose_name}", | ||||
|             )[0] | ||||
|             role.permissions.set([permission]) | ||||
|             role.save() | ||||
|  | ||||
|             # Check that the access is possible | ||||
|             resp = self.client.get(url + f"{obj.pk}/") | ||||
|             self.assertEqual(resp.status_code, 200, f"Permission {permission.query} is not working " | ||||
|                                                     f"for the model {obj._meta.verbose_name}") | ||||
|  | ||||
|         # Restore rights | ||||
|         self.user.is_superuser = True | ||||
|         self.user.save() | ||||
|         sess = self.client.session | ||||
|         sess["permission_mask"] = 42 | ||||
|         sess.save() | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_value(obj, key: str): | ||||
|         """ | ||||
|         Resolve the queryset filter to get the Python value of an object. | ||||
|         """ | ||||
|         if hasattr(obj, "all"): | ||||
|             # obj is a RelatedManager | ||||
|             obj = obj.last() | ||||
|  | ||||
|         if obj is None:  # pragma: no cover | ||||
|             return None | ||||
|  | ||||
|         if '__' not in key: | ||||
|             obj = getattr(obj, key) | ||||
|             if hasattr(obj, "pk"): | ||||
|                 return obj.pk | ||||
|             elif hasattr(obj, "all"): | ||||
|                 if not obj.exists():  # pragma: no cover | ||||
|                     return None | ||||
|                 return obj.last().pk | ||||
|             elif isinstance(obj, bool): | ||||
|                 return int(obj) | ||||
|             elif isinstance(obj, datetime): | ||||
|                 return obj.isoformat() | ||||
|             elif isinstance(obj, PhoneNumber): | ||||
|                 return obj.raw_input | ||||
|             return obj | ||||
|  | ||||
|         key, remaining = key.split('__', 1) | ||||
|         return TestAPI.get_value(getattr(obj, key), remaining) | ||||
|  | ||||
|     @staticmethod | ||||
|     def fix_note_object(obj, field): | ||||
|         """ | ||||
|         When querying an object that has a noteclub or a noteuser field, | ||||
|         ensure that the object has a good value. | ||||
|         """ | ||||
|         if isinstance(obj, Alias): | ||||
|             if "noteuser" in field: | ||||
|                 return NoteUser.objects.last().alias.last() | ||||
|             elif "noteclub" in field: | ||||
|                 return NoteClub.objects.last().alias.last() | ||||
|         elif isinstance(obj, Note): | ||||
|             if "noteuser" in field: | ||||
|                 return NoteUser.objects.last() | ||||
|             elif "noteclub" in field: | ||||
|                 return NoteClub.objects.last() | ||||
|         return obj | ||||
|  | ||||
|  | ||||
| class TestBasicAPI(TestAPI): | ||||
|     def test_user_api(self): | ||||
|         """ | ||||
|         Load the user page. | ||||
|         """ | ||||
|         self.check_viewset(ContentTypeViewSet, "/api/models/") | ||||
|         self.check_viewset(UserViewSet, "/api/user/") | ||||
| @@ -6,6 +6,7 @@ from django_filters.rest_framework import DjangoFilterBackend | ||||
| from django.db.models import Q | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from rest_framework.filters import SearchFilter | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet | ||||
| from permission.backends import PermissionBackend | ||||
| from note_kfet.middlewares import get_current_session | ||||
| @@ -48,12 +49,13 @@ class UserViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/users/ | ||||
|     then render it on /api/user/ | ||||
|     """ | ||||
|     queryset = User.objects.all() | ||||
|     queryset = User.objects | ||||
|     serializer_class = UserSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ] | ||||
|     filterset_fields = ['id', 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', | ||||
|                         'note__alias__name', 'note__alias__normalized_name', ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         queryset = super().get_queryset() | ||||
| @@ -106,7 +108,10 @@ class ContentTypeViewSet(ReadOnlyModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/users/ | ||||
|     then render it on /api/models/ | ||||
|     """ | ||||
|     queryset = ContentType.objects.all() | ||||
|     queryset = ContentType.objects.order_by('id') | ||||
|     serializer_class = ContentTypeSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['id', 'app_label', 'model', ] | ||||
|     search_fields = ['$app_label', '$model', ] | ||||
|   | ||||
| @@ -15,7 +15,7 @@ class ChangelogViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/logs/ | ||||
|     """ | ||||
|     queryset = Changelog.objects.all() | ||||
|     queryset = Changelog.objects.order_by('id') | ||||
|     serializer_class = ChangelogSerializer | ||||
|     filter_backends = [DjangoFilterBackend, OrderingFilter] | ||||
|     filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ] | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework.filters import SearchFilter | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
|  | ||||
| from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer | ||||
| @@ -14,8 +15,15 @@ class ProfileViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/members/profile/ | ||||
|     """ | ||||
|     queryset = Profile.objects.all() | ||||
|     queryset = Profile.objects.order_by('id') | ||||
|     serializer_class = ProfileSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email', | ||||
|                         'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section", | ||||
|                         'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration', | ||||
|                         'ml_art_registration', 'report_frequency', 'email_confirmed', 'registration_valid', ] | ||||
|     search_fields = ['$user__first_name', '$user__last_name', '$user__username', '$user__email', | ||||
|                      '$user__note__alias__name', '$user__note__alias__normalized_name', ] | ||||
|  | ||||
|  | ||||
| class ClubViewSet(ReadProtectedModelViewSet): | ||||
| @@ -24,10 +32,13 @@ class ClubViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/members/club/ | ||||
|     """ | ||||
|     queryset = Club.objects.all() | ||||
|     queryset = Club.objects.order_by('id') | ||||
|     serializer_class = ClubSerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$name', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club', | ||||
|                         'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid', | ||||
|                         'membership_duration', 'membership_start', 'membership_end', ] | ||||
|     search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ] | ||||
|  | ||||
|  | ||||
| class MembershipViewSet(ReadProtectedModelViewSet): | ||||
| @@ -36,5 +47,14 @@ class MembershipViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/members/membership/ | ||||
|     """ | ||||
|     queryset = Membership.objects.all() | ||||
|     queryset = Membership.objects.order_by('id') | ||||
|     serializer_class = MembershipSerializer | ||||
|     filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|     filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name', | ||||
|                         'user__username', 'user__last_name', 'user__first_name', 'user__email', | ||||
|                         'user__note__alias__name', 'user__note__alias__normalized_name', | ||||
|                         'date_start', 'date_end', 'fee', 'roles', ] | ||||
|     ordering_fields = ['id', 'date_start', 'date_end', ] | ||||
|     search_fields = ['$club__name', '$club__email', '$club__note__alias__name', '$club__note__alias__normalized_name', | ||||
|                      '$user__username', '$user__last_name', '$user__first_name', '$user__email', | ||||
|                      '$user__note__alias__name', '$user__note__alias__normalized_name', '$roles__name', ] | ||||
|   | ||||
| @@ -8,6 +8,7 @@ from django import forms | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.forms import AuthenticationForm | ||||
| from django.contrib.auth.models import User | ||||
| from django.db import transaction | ||||
| from django.forms import CheckboxSelectMultiple | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| @@ -57,6 +58,7 @@ class ProfileForm(forms.ModelForm): | ||||
|         self.fields['address'].widget.attrs.update({"placeholder": "4 avenue des Sciences, 91190 GIF-SUR-YVETTE"}) | ||||
|         self.fields['promotion'].widget.attrs.update({"max": timezone.now().year}) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, commit=True): | ||||
|         if not self.instance.section or (("department" in self.changed_data | ||||
|                                          or "promotion" in self.changed_data) and "section" not in self.changed_data): | ||||
| @@ -148,6 +150,7 @@ class ClubForm(forms.ModelForm): | ||||
|             "membership_fee_unpaid": AmountInput(), | ||||
|             "parent_club": Autocomplete( | ||||
|                 Club, | ||||
|                 resetable=True, | ||||
|                 attrs={ | ||||
|                     'api_url': '/api/members/club/', | ||||
|                 } | ||||
| @@ -161,7 +164,7 @@ class MembershipForm(forms.ModelForm): | ||||
|     soge = forms.BooleanField( | ||||
|         label=_("Inscription paid by Société Générale"), | ||||
|         required=False, | ||||
|         help_text=_("Check this case is the Société Générale paid the inscription."), | ||||
|         help_text=_("Check this case if the Société Générale paid the inscription."), | ||||
|     ) | ||||
|  | ||||
|     credit_type = forms.ModelChoiceField( | ||||
|   | ||||
| @@ -7,6 +7,7 @@ def create_bde_and_kfet(apps, schema_editor): | ||||
|     """ | ||||
|     Club = apps.get_model("member", "club") | ||||
|     NoteClub = apps.get_model("note", "noteclub") | ||||
|     Alias = apps.get_model("note", "alias") | ||||
|     ContentType = apps.get_model('contenttypes', 'ContentType') | ||||
|     polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id | ||||
|  | ||||
| @@ -45,6 +46,19 @@ def create_bde_and_kfet(apps, schema_editor): | ||||
|         polymorphic_ctype_id=polymorphic_ctype_id, | ||||
|     ) | ||||
|  | ||||
|     Alias.objects.get_or_create( | ||||
|         id=5, | ||||
|         note_id=5, | ||||
|         name="BDE", | ||||
|         normalized_name="bde", | ||||
|     ) | ||||
|     Alias.objects.get_or_create( | ||||
|         id=6, | ||||
|         note_id=6, | ||||
|         name="Kfet", | ||||
|         normalized_name="kfet", | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| import sys | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| def give_note_account_permissions(apps, schema_editor): | ||||
|     """ | ||||
|     Automatically manage the membership of the Note account. | ||||
|     """ | ||||
|     User = apps.get_model("auth", "user") | ||||
|     Membership = apps.get_model("member", "membership") | ||||
|     Role = apps.get_model("permission", "role") | ||||
|  | ||||
|     note = User.objects.filter(username="note") | ||||
|     if not note.exists(): | ||||
|         # We are in a test environment, don't log error message | ||||
|         if len(sys.argv) > 1 and sys.argv[1] == 'test': | ||||
|             return | ||||
|         print("Warning: Note account was not found. The note account was not imported.") | ||||
|         print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.") | ||||
|         print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you " | ||||
|               "don't want this account.") | ||||
|         return | ||||
|  | ||||
|     note = note.get() | ||||
|  | ||||
|     # Set for the two clubs a large expiration date and the correct role. | ||||
|     for m in Membership.objects.filter(user_id=note.id).all(): | ||||
|         m.date_end = "3142-12-12" | ||||
|         m.roles.set(Role.objects.filter(name="PC Kfet").all()) | ||||
|         m.save() | ||||
|     # By default, the note account is only authorized to be logged from localhost. | ||||
|     note.password = "ipbased$127.0.0.1" | ||||
|     note.is_active = True | ||||
|     note.save() | ||||
|     # Ensure that the note of the account is disabled | ||||
|     note.note.inactivity_reason = 'forced' | ||||
|     note.note.is_active = False | ||||
|     note.save() | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|     dependencies = [ | ||||
|         ('member', '0005_remove_null_tag_on_charfields'), | ||||
|         ('permission', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython(give_note_account_permissions), | ||||
|     ] | ||||
| @@ -7,7 +7,7 @@ import os | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.db import models, transaction | ||||
| from django.db.models import Q | ||||
| from django.template import loader | ||||
| from django.urls import reverse, reverse_lazy | ||||
| @@ -271,6 +271,7 @@ class Club(models.Model): | ||||
|             self._force_save = True | ||||
|             self.save(force_update=True) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, force_insert=False, force_update=False, using=None, | ||||
|              update_fields=None): | ||||
|         if not self.require_memberships: | ||||
| @@ -312,6 +313,7 @@ class Membership(models.Model): | ||||
|  | ||||
|     roles = models.ManyToManyField( | ||||
|         "permission.Role", | ||||
|         related_name="memberships", | ||||
|         verbose_name=_("roles"), | ||||
|     ) | ||||
|  | ||||
| @@ -406,6 +408,7 @@ class Membership(models.Model): | ||||
|                 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. | ||||
| @@ -475,8 +478,13 @@ class Membership(models.Model): | ||||
|                 # to treasurers. | ||||
|                 transaction.valid = False | ||||
|                 from treasury.models import SogeCredit | ||||
|                 soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0] | ||||
|                 soge_credit.refresh_from_db() | ||||
|                 if SogeCredit.objects.filter(user=self.user).exists(): | ||||
|                     soge_credit = SogeCredit.objects.get(user=self.user) | ||||
|                 else: | ||||
|                     soge_credit = SogeCredit(user=self.user) | ||||
|                     soge_credit._force_save = True | ||||
|                     soge_credit.save(force_insert=True) | ||||
|                     soge_credit.refresh_from_db() | ||||
|                 transaction.save(force_insert=True) | ||||
|                 transaction.refresh_from_db() | ||||
|                 soge_credit.transactions.add(transaction) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ function create_alias (e) { | ||||
|   }).done(function () { | ||||
|     // Reload table | ||||
|     $('#alias_table').load(location.pathname + ' #alias_table') | ||||
|     addMsg('Alias ajouté', 'success') | ||||
|     addMsg(gettext('Alias successfully added'), 'success') | ||||
|   }).fail(function (xhr, _textStatus, _error) { | ||||
|     errMsg(xhr.responseJSON) | ||||
|   }) | ||||
| @@ -22,7 +22,7 @@ function create_alias (e) { | ||||
|  | ||||
| /** | ||||
|  * On click of "delete", delete the alias | ||||
|  * @param Integer button_id Alias id to remove | ||||
|  * @param button_id:Integer Alias id to remove | ||||
|  */ | ||||
| function delete_button (button_id) { | ||||
|   $.ajax({ | ||||
| @@ -30,7 +30,7 @@ function delete_button (button_id) { | ||||
|     method: 'DELETE', | ||||
|     headers: { 'X-CSRFTOKEN': CSRF_TOKEN } | ||||
|   }).done(function () { | ||||
|     addMsg('Alias supprimé', 'success') | ||||
|     addMsg(gettext('Alias successfully deleted'), 'success') | ||||
|     $('#alias_table').load(location.pathname + ' #alias_table') | ||||
|   }).fail(function (xhr, _textStatus, _error) { | ||||
|     errMsg(xhr.responseJSON) | ||||
|   | ||||
| @@ -43,8 +43,24 @@ class UserTable(tables.Table): | ||||
|  | ||||
|     section = tables.Column(accessor='profile__section') | ||||
|  | ||||
|     # Override the column to let replace the URL | ||||
|     email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email)) | ||||
|  | ||||
|     balance = tables.Column(accessor='note__balance', verbose_name=_("Balance")) | ||||
|  | ||||
|     def render_email(self, record, value): | ||||
|         # Replace the email by a dash if the user can't see the profile detail | ||||
|         # Replace also the URL | ||||
|         if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile): | ||||
|             value = "—" | ||||
|             record.email = value | ||||
|         return value | ||||
|  | ||||
|     def render_section(self, record, value): | ||||
|         return value \ | ||||
|             if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \ | ||||
|             else "—" | ||||
|  | ||||
|     def render_balance(self, record, value): | ||||
|         return pretty_money(value)\ | ||||
|             if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—" | ||||
| @@ -112,7 +128,7 @@ class MembershipTable(tables.Table): | ||||
|                     fee=0, | ||||
|                 ) | ||||
|                 if PermissionBackend.check_perm(get_current_authenticated_user(), | ||||
|                                                 "member:add_membership", empty_membership):  # If the user has right | ||||
|                                                 "member.add_membership", empty_membership):  # If the user has right | ||||
|                     renew_url = reverse_lazy('member:club_renew_membership', | ||||
|                                              kwargs={"pk": record.pk}) | ||||
|                     t = format_html( | ||||
|   | ||||
| @@ -13,15 +13,29 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         {% if additional_fee_renewal %} | ||||
|         <div class="alert alert-warning"> | ||||
|             {% if renewal %} | ||||
|             {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|             The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }} | ||||
|             will be charged to renew automatically the membership in this/these club·s. | ||||
|             {% endblocktrans %} | ||||
|                 {% if club.name == "Kfet" %} {# Auto-renewal #} | ||||
|                     {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|                     The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }} | ||||
|                     will be charged to renew automatically the membership in this/these club·s. | ||||
|                     {% endblocktrans %} | ||||
|                 {% else %} | ||||
|                     {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|                         The user is not a member of the club·s {{ clubs }}. Please create the required memberships, | ||||
|                         otherwise it will fail. | ||||
|                     {% endblocktrans %} | ||||
|                 {% endif %} | ||||
|             {% else %} | ||||
|             {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|             This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }} | ||||
|             will be charged to adhere automatically to this/these club·s. | ||||
|             {% endblocktrans %} | ||||
|                 {% if club.name == "Kfet" %} | ||||
|                     {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|                     This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }} | ||||
|                     will be charged to adhere automatically to this/these club·s. | ||||
|                     {% endblocktrans %} | ||||
|                 {% else %} | ||||
|                     {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} | ||||
|                         This club has parents {{ clubs }}. Please make sure that the user is a member of this or these club·s, | ||||
|                         otherwise the creation of this membership will fail. | ||||
|                     {% endblocktrans %} | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|         </div> | ||||
|         {% endif %} | ||||
|   | ||||
| @@ -48,7 +48,7 @@ | ||||
|     <dd class="col-xl-6"> | ||||
|         <a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}"> | ||||
|             <i class="fa fa-edit"></i> | ||||
|             {% trans 'Manage aliases' %} ({{ club.note.alias_set.all|length }}) | ||||
|             {% trans 'Manage aliases' %} ({{ club.note.alias.all|length }}) | ||||
|         </a> | ||||
|     </dd> | ||||
|  | ||||
|   | ||||
| @@ -21,33 +21,35 @@ | ||||
|     <dd class="col-xl-6"> | ||||
|         <a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}"> | ||||
|             <i class="fa fa-edit"></i> | ||||
|             {% trans 'Manage aliases' %} ({{ user_object.note.alias_set.all|length }}) | ||||
|             {% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }}) | ||||
|         </a> | ||||
|     </dd> | ||||
|  | ||||
|     <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6">{{ user_object.profile.section }}</dd> | ||||
|     {% if "member.view_profile"|has_perm:user_object.profile %} | ||||
|         <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6">{{ user_object.profile.section }}</dd> | ||||
|  | ||||
|     <dt class="col-xl-6">{% trans 'email'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd> | ||||
|         <dt class="col-xl-6">{% trans 'email'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd> | ||||
|  | ||||
|     <dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a> | ||||
|     </dd> | ||||
|         <dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a> | ||||
|         </dd> | ||||
|  | ||||
|     <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6">{{ user_object.profile.address }}</dd> | ||||
|         <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6">{{ user_object.profile.address }}</dd> | ||||
|  | ||||
|     {% if "note.view_note"|has_perm:user_object.note %} | ||||
|     <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd> | ||||
|         {% if user_object.note and "note.view_note"|has_perm:user_object.note %} | ||||
|         <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd> | ||||
|  | ||||
|     <dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt> | ||||
|     <dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd> | ||||
|         <dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt> | ||||
|         <dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd> | ||||
|         {% endif %} | ||||
|     {% endif %} | ||||
| </dl> | ||||
|  | ||||
| {% if user_object.pk == user_object.pk %} | ||||
| {% if user_object.pk == user.pk %} | ||||
|     <div class="text-center"> | ||||
|         <a class="small badge badge-secondary" href="{% url 'member:auth_token' %}"> | ||||
|             <i class="fa fa-cogs"></i>{% trans 'API token' %} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% load i18n perms %} | ||||
|  | ||||
| {% block content %} | ||||
| {% if "member.change_profile_registration_valid"|has_perm:user %} | ||||
| {% if can_manage_registrations %} | ||||
| <a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}"> | ||||
|     <i class="fa fa-user-plus"></i> {% trans "Registrations" %} | ||||
| </a> | ||||
|   | ||||
							
								
								
									
										0
									
								
								apps/member/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/member/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										22
									
								
								apps/member/templatetags/memberinfo.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								apps/member/templatetags/memberinfo.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
|  | ||||
| from django import template | ||||
| from django.contrib.auth.models import User | ||||
|  | ||||
| from ..models import Club, Membership | ||||
|  | ||||
|  | ||||
| def is_member(user, club): | ||||
|     if isinstance(user, str): | ||||
|         club = User.objects.get(username=user) | ||||
|     if isinstance(club, str): | ||||
|         club = Club.objects.get(name=club) | ||||
|     return Membership.objects\ | ||||
|         .filter(user=user, club=club, date_start__lte=date.today(), date_end__gte=date.today()).exists() | ||||
|  | ||||
|  | ||||
| register = template.Library() | ||||
| register.filter("is_member", is_member) | ||||
| @@ -41,7 +41,7 @@ class TemplateLoggedInTests(TestCase): | ||||
|             password="adminadmin", | ||||
|             permission_mask=3, | ||||
|         )) | ||||
|         self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200) | ||||
|         self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302) | ||||
|  | ||||
|     def test_logout(self): | ||||
|         response = self.client.get(reverse("logout")) | ||||
|   | ||||
| @@ -5,17 +5,20 @@ import hashlib | ||||
| import os | ||||
| from datetime import date, timedelta | ||||
|  | ||||
| from api.tests import TestAPI | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.files.uploadedfile import SimpleUploadedFile | ||||
| from django.db.models import Q | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
| from django.utils import timezone | ||||
| from member.models import Club, Membership, Profile | ||||
| from note.models import Alias, NoteSpecial | ||||
| from permission.models import Role | ||||
| from treasury.models import SogeCredit | ||||
|  | ||||
| from ..api.views import ClubViewSet, MembershipViewSet, ProfileViewSet | ||||
| from ..models import Club, Membership, Profile | ||||
|  | ||||
| """ | ||||
| Create some users and clubs and test that all pages are rendering properly | ||||
| and that memberships are working. | ||||
| @@ -205,7 +208,7 @@ class TestMemberships(TestCase): | ||||
|                 first_name="Toto", | ||||
|                 bank="Le matelas", | ||||
|             )) | ||||
|             self.assertRedirects(response, club.get_absolute_url(), 302, 200) | ||||
|             self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200) | ||||
|  | ||||
|             self.assertTrue(Membership.objects.filter(user=user, club=club).exists()) | ||||
|  | ||||
| @@ -244,9 +247,9 @@ class TestMemberships(TestCase): | ||||
|                 first_name="Toto", | ||||
|                 bank="Bank", | ||||
|             )) | ||||
|             self.assertRedirects(response, club.get_absolute_url(), 302, 200) | ||||
|             self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200) | ||||
|  | ||||
|             response = self.client.get(user.profile.get_absolute_url()) | ||||
|             response = self.client.get(club.get_absolute_url()) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_auto_join_kfet_when_join_bde_with_soge(self): | ||||
| @@ -273,7 +276,7 @@ class TestMemberships(TestCase): | ||||
|             first_name="Toto", | ||||
|             bank="Société générale", | ||||
|         )) | ||||
|         self.assertRedirects(response, bde.get_absolute_url(), 302, 200) | ||||
|         self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200) | ||||
|  | ||||
|         self.assertTrue(Membership.objects.filter(user=user, club=bde).exists()) | ||||
|         self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists()) | ||||
| @@ -403,3 +406,46 @@ class TestMemberships(TestCase): | ||||
|         self.user.password = "custom_nk15$1$" + salt + "|" + hashed | ||||
|         self.user.save() | ||||
|         self.assertTrue(self.user.check_password(password)) | ||||
|  | ||||
|  | ||||
| class TestMemberAPI(TestAPI): | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|  | ||||
|         self.user.profile.registration_valid = True | ||||
|         self.user.profile.email_confirmed = True | ||||
|         self.user.profile.phone_number = "0600000000" | ||||
|         self.user.profile.section = "1A0" | ||||
|         self.user.profile.department = "A0" | ||||
|         self.user.profile.address = "Earth" | ||||
|         self.user.profile.save() | ||||
|  | ||||
|         self.club = Club.objects.create( | ||||
|             name="totoclub", | ||||
|             parent_club=Club.objects.get(name="BDE"), | ||||
|             membership_start=date(year=1970, month=1, day=1), | ||||
|             membership_end=date(year=2040, month=1, day=1), | ||||
|             membership_duration=365 * 10, | ||||
|         ) | ||||
|         self.bde_membership = Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) | ||||
|         self.membership = Membership.objects.create(user=self.user, club=self.club) | ||||
|         self.membership.roles.add(Role.objects.get(name="Bureau de club")) | ||||
|         self.membership.save() | ||||
|  | ||||
|     def test_club_api(self): | ||||
|         """ | ||||
|         Load Club API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ClubViewSet, "/api/members/club/") | ||||
|  | ||||
|     def test_profile_api(self): | ||||
|         """ | ||||
|         Load Profile API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ProfileViewSet, "/api/members/profile/") | ||||
|  | ||||
|     def test_membership_api(self): | ||||
|         """ | ||||
|         Load Membership API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(MembershipViewSet, "/api/members/membership/") | ||||
|   | ||||
| @@ -38,6 +38,7 @@ class CustomLoginView(LoginView): | ||||
|     """ | ||||
|     form_class = CustomAuthenticationForm | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         logout(self.request) | ||||
|         _set_current_user_and_ip(form.get_user(), self.request.session, None) | ||||
| @@ -69,13 +70,15 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|         form.fields['email'].required = True | ||||
|         form.fields['email'].help_text = _("This address must be valid.") | ||||
|  | ||||
|         context['profile_form'] = self.profile_form(instance=context['user_object'].profile, | ||||
|                                                     data=self.request.POST if self.request.POST else None) | ||||
|         if not self.object.profile.report_frequency: | ||||
|             del context['profile_form'].fields["last_report"] | ||||
|         if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile): | ||||
|             context['profile_form'] = self.profile_form(instance=context['user_object'].profile, | ||||
|                                                         data=self.request.POST if self.request.POST else None) | ||||
|             if not self.object.profile.report_frequency: | ||||
|                 del context['profile_form'].fields["last_report"] | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Check if ProfileForm is correct | ||||
| @@ -155,8 +158,12 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|         history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) | ||||
|         context['history_list'] = history_table | ||||
|  | ||||
|         club_list = Membership.objects.filter(user=user, date_end__gte=date.today())\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) | ||||
|         club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\ | ||||
|             .order_by("club__name", "-date_start") | ||||
|         # Display only the most recent membership | ||||
|         club_list = club_list.distinct("club__name")\ | ||||
|             if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list | ||||
|         membership_table = MembershipTable(data=club_list, prefix='membership-') | ||||
|         membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1)) | ||||
|         context['club_list'] = membership_table | ||||
| @@ -164,6 +171,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|         # Check permissions to see if the authenticated user can lock/unlock the note | ||||
|         with transaction.atomic(): | ||||
|             modified_note = NoteUser.objects.get(pk=user.note.pk) | ||||
|             # Don't log these tests | ||||
|             modified_note._no_signal = True | ||||
|             modified_note.is_active = True | ||||
|             modified_note.inactivity_reason = 'manual' | ||||
|             context["can_lock_note"] = user.note.is_active and PermissionBackend\ | ||||
| @@ -176,6 +185,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|             context["can_force_lock"] = user.note.is_active and PermissionBackend\ | ||||
|                 .check_perm(self.request.user, "note.change_note_is_active", modified_note) | ||||
|             old_note._force_save = True | ||||
|             old_note._no_signal = True | ||||
|             old_note.save() | ||||
|             modified_note.refresh_from_db() | ||||
|             modified_note.is_active = True | ||||
| @@ -225,6 +235,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|  | ||||
|         return qs | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\ | ||||
|             .filter(profile__registration_valid=False) | ||||
|         context["can_manage_registrations"] = pre_registered_users.exists() | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     """ | ||||
| @@ -238,8 +255,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         note = context['object'].note | ||||
|         context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend | ||||
|                                                               .filter_queryset(self.request.user, Alias, "view")).all()) | ||||
|         context["aliases"] = AliasTable( | ||||
|             note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) | ||||
|         context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( | ||||
|             note=context["object"].note, | ||||
|             name="", | ||||
| @@ -269,6 +286,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det | ||||
|         self.object = self.get_object() | ||||
|         return self.form_valid(form) if form.is_valid() else self.form_invalid(form) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """Save image to note""" | ||||
|         image = form.cleaned_data['image'] | ||||
| @@ -389,7 +407,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|         if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): | ||||
|             club.update_membership_dates() | ||||
|         # managers list | ||||
|         managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\ | ||||
|         managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club", | ||||
|                                              date_start__lte=date.today(), date_end__gte=date.today())\ | ||||
|             .order_by('user__last_name').all() | ||||
|         context["managers"] = ClubManagerTable(data=managers, prefix="managers-") | ||||
|         # transaction history | ||||
| @@ -402,8 +421,12 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|         # member list | ||||
|         club_member = Membership.objects.filter( | ||||
|             club=club, | ||||
|             date_end__gte=date.today(), | ||||
|         ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) | ||||
|             date_end__gte=date.today() - timedelta(days=15), | ||||
|         ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\ | ||||
|             .order_by("user__username", "-date_start") | ||||
|         # Display only the most recent membership | ||||
|         club_member = club_member.distinct("user__username")\ | ||||
|             if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member | ||||
|  | ||||
|         membership_table = MembershipTable(data=club_member, prefix="membership-") | ||||
|         membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1)) | ||||
| @@ -435,8 +458,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         note = context['object'].note | ||||
|         context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend | ||||
|                                                               .filter_queryset(self.request.user, Alias, "view")).all()) | ||||
|         context["aliases"] = AliasTable(note.alias.filter( | ||||
|             PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all()) | ||||
|         context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( | ||||
|             note=context["object"].note, | ||||
|             name="", | ||||
| @@ -607,6 +630,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         bank = form.cleaned_data["bank"] | ||||
|         soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet") | ||||
|  | ||||
|         if not credit_type: | ||||
|             credit_amount = 0 | ||||
|  | ||||
|         if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter( | ||||
|                 club__name="Kfet", | ||||
|                 user=user, | ||||
| @@ -628,6 +654,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             form.add_error('user', _('User is already a member of the club')) | ||||
|             error = True | ||||
|  | ||||
|         # Must join the parent club before joining this club, except for the Kfet club where it can be at the same time. | ||||
|         if club.name != "Kfet" and club.parent_club and not Membership.objects.filter( | ||||
|                 user=form.instance.user, | ||||
|                 club=club.parent_club, | ||||
|                 date_start__gte=club.parent_club.membership_start, | ||||
|                 date_end__lte=club.parent_club.membership_end, | ||||
|         ).exists(): | ||||
|             form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name) | ||||
|             error = True | ||||
|  | ||||
|         if club.membership_start and form.instance.date_start < club.membership_start: | ||||
|             form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
| @@ -642,14 +678,17 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): | ||||
|                 if not last_name: | ||||
|                     form.add_error('last_name', _("This field is required.")) | ||||
|                     error = True | ||||
|                 if not first_name: | ||||
|                     form.add_error('first_name', _("This field is required.")) | ||||
|                     error = True | ||||
|                 if not bank and credit_type.special_type == "Chèque": | ||||
|                     form.add_error('bank', _("This field is required.")) | ||||
|                 return self.form_invalid(form) | ||||
|                     error = True | ||||
|  | ||||
|         return not error | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Create membership, check that all is good, make transactions | ||||
| @@ -659,6 +698,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \ | ||||
|                 .get(pk=self.kwargs["club_pk"]) | ||||
|             user = form.instance.user | ||||
|             old_membership = None | ||||
|         else:  # get from url for renewal | ||||
|             old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) | ||||
|             club = old_membership.club | ||||
| @@ -733,6 +773,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         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() \ | ||||
|             if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() | ||||
|         # Set the same roles as before | ||||
|         if old_membership: | ||||
|             member_role = member_role.union(old_membership.roles.all()) | ||||
|         form.instance.roles.set(member_role) | ||||
|         form.instance._force_save = True | ||||
|         form.instance.save() | ||||
| @@ -770,7 +813,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         return ret | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) | ||||
|         return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) | ||||
|  | ||||
|  | ||||
| class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db.models import Q | ||||
| from django.core.exceptions import ValidationError | ||||
| @@ -14,29 +15,37 @@ from permission.backends import PermissionBackend | ||||
|  | ||||
| from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ | ||||
|     TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer | ||||
| from ..models.notes import Note, Alias | ||||
| from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial | ||||
| from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory | ||||
|  | ||||
|  | ||||
| class NotePolymorphicViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, | ||||
|     The djangorestframework plugin will get all `Note` objects (with polymorhism), | ||||
|     serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/note/ | ||||
|     """ | ||||
|     queryset = Note.objects.all() | ||||
|     queryset = Note.objects.order_by('id') | ||||
|     serializer_class = NotePolymorphicSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] | ||||
|     filterset_fields = ['polymorphic_ctype', 'is_active', ] | ||||
|     search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ] | ||||
|     ordering_fields = ['alias__name', 'alias__normalized_name'] | ||||
|     filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ] | ||||
|     search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', | ||||
|                      '$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email', | ||||
|                      '$noteuser__user__email', '$noteclub__club__email', ] | ||||
|     ordering_fields = ['alias__name', 'alias__normalized_name', 'balance', 'created_at', ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         """ | ||||
|         Parse query and apply filters. | ||||
|         :return: The filtered set of requested notes | ||||
|         """ | ||||
|         queryset = super().get_queryset().distinct() | ||||
|         user = self.request.user | ||||
|         get_current_session().setdefault("permission_mask", 42) | ||||
|         queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view") | ||||
|                                         | PermissionBackend.filter_queryset(user, NoteUser, "view") | ||||
|                                         | PermissionBackend.filter_queryset(user, NoteClub, "view") | ||||
|                                         | PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct() | ||||
|  | ||||
|         alias = self.request.query_params.get("alias", ".*") | ||||
|         queryset = queryset.filter( | ||||
| @@ -54,11 +63,12 @@ class AliasViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/aliases/ | ||||
|     """ | ||||
|     queryset = Alias.objects.all() | ||||
|     queryset = Alias.objects | ||||
|     serializer_class = AliasSerializer | ||||
|     filter_backends = [SearchFilter, OrderingFilter] | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] | ||||
|     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] | ||||
|     ordering_fields = ['name', 'normalized_name'] | ||||
|     filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ] | ||||
|     ordering_fields = ['name', 'normalized_name', ] | ||||
|  | ||||
|     def get_serializer_class(self): | ||||
|         serializer_class = self.serializer_class | ||||
| @@ -104,11 +114,12 @@ class AliasViewSet(ReadProtectedModelViewSet): | ||||
|  | ||||
|  | ||||
| class ConsumerViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     queryset = Alias.objects.all() | ||||
|     queryset = Alias.objects | ||||
|     serializer_class = ConsumerSerializer | ||||
|     filter_backends = [SearchFilter, OrderingFilter] | ||||
|     filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] | ||||
|     ordering_fields = ['name', 'normalized_name'] | ||||
|     filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ] | ||||
|     ordering_fields = ['name', 'normalized_name', ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         """ | ||||
| @@ -116,29 +127,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet): | ||||
|         :return: The filtered set of requested aliases | ||||
|         """ | ||||
|  | ||||
|         queryset = super().get_queryset() | ||||
|         queryset = super().get_queryset().distinct() | ||||
|         # Sqlite doesn't support ORDER BY in subqueries | ||||
|         queryset = queryset.order_by("name") \ | ||||
|             if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset | ||||
|  | ||||
|         alias = self.request.query_params.get("alias", ".*") | ||||
|         alias = self.request.query_params.get("alias", None) | ||||
|         queryset = queryset.prefetch_related('note') | ||||
|         # We match first an alias if it is matched without normalization, | ||||
|         # then if the normalized pattern matches a normalized alias. | ||||
|         queryset = queryset.filter( | ||||
|             name__iregex="^" + alias | ||||
|         ).union( | ||||
|             queryset.filter( | ||||
|                 Q(normalized_name__iregex="^" + Alias.normalize(alias)) | ||||
|                 & ~Q(name__iregex="^" + alias) | ||||
|             ), | ||||
|             all=True).union( | ||||
|             queryset.filter( | ||||
|                 Q(normalized_name__iregex="^" + alias.lower()) | ||||
|                 & ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) | ||||
|                 & ~Q(name__iregex="^" + alias) | ||||
|             ), | ||||
|             all=True) | ||||
|  | ||||
|         if alias: | ||||
|             # We match first an alias if it is matched without normalization, | ||||
|             # then if the normalized pattern matches a normalized alias. | ||||
|             queryset = queryset.filter( | ||||
|                 name__iregex="^" + alias | ||||
|             ).union( | ||||
|                 queryset.filter( | ||||
|                     Q(normalized_name__iregex="^" + Alias.normalize(alias)) | ||||
|                     & ~Q(name__iregex="^" + alias) | ||||
|                 ), | ||||
|                 all=True).union( | ||||
|                 queryset.filter( | ||||
|                     Q(normalized_name__iregex="^" + alias.lower()) | ||||
|                     & ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) | ||||
|                     & ~Q(name__iregex="^" + alias) | ||||
|                 ), | ||||
|                 all=True) | ||||
|  | ||||
|         queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ | ||||
|             else queryset.order_by("name") | ||||
| @@ -152,10 +165,11 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/transaction/category/ | ||||
|     """ | ||||
|     queryset = TemplateCategory.objects.order_by("name").all() | ||||
|     queryset = TemplateCategory.objects.order_by('name') | ||||
|     serializer_class = TemplateCategorySerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$name', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'templates', 'templates__name'] | ||||
|     search_fields = ['$name', '$templates__name', ] | ||||
|  | ||||
|  | ||||
| class TransactionTemplateViewSet(viewsets.ModelViewSet): | ||||
| @@ -164,11 +178,12 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet): | ||||
|     The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/transaction/template/ | ||||
|     """ | ||||
|     queryset = TransactionTemplate.objects.order_by("name").all() | ||||
|     queryset = TransactionTemplate.objects.order_by('name') | ||||
|     serializer_class = TransactionTemplateSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     filterset_fields = ['name', 'amount', 'display', 'category', ] | ||||
|     search_fields = ['$name', ] | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] | ||||
|     filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ] | ||||
|     search_fields = ['$name', '$category__name', ] | ||||
|     ordering_fields = ['amount', ] | ||||
|  | ||||
|  | ||||
| class TransactionViewSet(ReadProtectedModelViewSet): | ||||
| @@ -177,10 +192,17 @@ class TransactionViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/transaction/transaction/ | ||||
|     """ | ||||
|     queryset = Transaction.objects.order_by("-created_at").all() | ||||
|     queryset = Transaction.objects.order_by('-created_at') | ||||
|     serializer_class = TransactionPolymorphicSerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$reason', ] | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] | ||||
|     filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name', | ||||
|                         'destination', 'destination_alias', 'destination__alias__name', | ||||
|                         'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount', | ||||
|                         'created_at', 'valid', 'invalidity_reason', ] | ||||
|     search_fields = ['$reason', '$source_alias', '$source__alias__name', '$source__alias__normalized_name', | ||||
|                      '$destination_alias', '$destination__alias__name', '$destination__alias__normalized_name', | ||||
|                      '$invalidity_reason', ] | ||||
|     ordering_fields = ['created_at', 'amount', ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         user = self.request.user | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from django.conf import settings | ||||
| from django.db.models.signals import post_save, pre_delete | ||||
| from django.db.models.signals import pre_delete, pre_save, post_save | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from . import signals | ||||
| @@ -17,6 +17,15 @@ class NoteConfig(AppConfig): | ||||
|         """ | ||||
|         Define app internal signals to interact with other apps | ||||
|         """ | ||||
|         pre_save.connect( | ||||
|             signals.pre_save_note, | ||||
|             sender="note.noteuser", | ||||
|         ) | ||||
|         pre_save.connect( | ||||
|             signals.pre_save_note, | ||||
|             sender="note.noteclub", | ||||
|         ) | ||||
|  | ||||
|         post_save.connect( | ||||
|             signals.save_user_note, | ||||
|             sender=settings.AUTH_USER_MODEL, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from django.conf.global_settings import DEFAULT_FROM_EMAIL | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.core.mail import send_mail | ||||
| from django.core.validators import RegexValidator | ||||
| from django.db import models | ||||
| from django.db import models, transaction | ||||
| from django.template.loader import render_to_string | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| @@ -93,6 +93,7 @@ class Note(PolymorphicModel): | ||||
|         delta = timezone.now() - self.last_negative | ||||
|         return "{:d} jours".format(delta.days) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         Save note with it's alias (called in polymorphic children) | ||||
| @@ -108,12 +109,16 @@ class Note(PolymorphicModel): | ||||
|  | ||||
|             # Save alias | ||||
|             a.note = self | ||||
|             # Consider that if the name of the note could be changed, then the alias can be created. | ||||
|             # It does not mean that any alias can be created. | ||||
|             a._force_save = True | ||||
|             a.save(force_insert=True) | ||||
|         else: | ||||
|             # Check if the name of the note changed without changing the normalized form of the alias | ||||
|             alias = Alias.objects.get(normalized_name=Alias.normalize(str(self))) | ||||
|             if alias.name != str(self): | ||||
|                 alias.name = str(self) | ||||
|                 alias._force_save = True | ||||
|                 alias.save() | ||||
|  | ||||
|     def clean(self, *args, **kwargs): | ||||
| @@ -154,19 +159,6 @@ class NoteUser(Note): | ||||
|     def pretty(self): | ||||
|         return _("%(user)s's note") % {'user': str(self.user)} | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.pk and self.balance < 0: | ||||
|             old_note = NoteUser.objects.get(pk=self.pk) | ||||
|             super().save(*args, **kwargs) | ||||
|             if old_note.balance >= 0: | ||||
|                 # Passage en négatif | ||||
|                 self.last_negative = timezone.now() | ||||
|                 self._force_save = True | ||||
|                 self.save(*args, **kwargs) | ||||
|                 self.send_mail_negative_balance() | ||||
|         else: | ||||
|             super().save(*args, **kwargs) | ||||
|  | ||||
|     def send_mail_negative_balance(self): | ||||
|         plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self)) | ||||
|         html = render_to_string("note/mails/negative_balance.html", dict(note=self)) | ||||
| @@ -195,19 +187,6 @@ class NoteClub(Note): | ||||
|     def pretty(self): | ||||
|         return _("Note of %(club)s club") % {'club': str(self.club)} | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.pk and self.balance < 0: | ||||
|             old_note = NoteClub.objects.get(pk=self.pk) | ||||
|             super().save(*args, **kwargs) | ||||
|             if old_note.balance >= 0: | ||||
|                 # Passage en négatif | ||||
|                 self.last_negative = timezone.now() | ||||
|                 self._force_save = True | ||||
|                 self.save(*args, **kwargs) | ||||
|                 self.send_mail_negative_balance() | ||||
|         else: | ||||
|             super().save(*args, **kwargs) | ||||
|  | ||||
|     def send_mail_negative_balance(self): | ||||
|         plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self)) | ||||
|         html = render_to_string("note/mails/negative_balance.html", dict(note=self)) | ||||
| @@ -269,6 +248,7 @@ class Alias(models.Model): | ||||
|     note = models.ForeignKey( | ||||
|         Note, | ||||
|         on_delete=models.PROTECT, | ||||
|         related_name="alias", | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
| @@ -310,6 +290,7 @@ class Alias(models.Model): | ||||
|             pass | ||||
|         self.normalized_name = normalized_name | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         self.clean() | ||||
|         super().save(*args, **kwargs) | ||||
|   | ||||
| @@ -170,19 +170,21 @@ class Transaction(PolymorphicModel): | ||||
|         previous_source_balance = self.source.balance | ||||
|         previous_dest_balance = self.destination.balance | ||||
|  | ||||
|         source_balance = self.source.balance | ||||
|         dest_balance = self.destination.balance | ||||
|         source_balance = previous_source_balance | ||||
|         dest_balance = previous_dest_balance | ||||
|  | ||||
|         created = self.pk is None | ||||
|         to_transfer = self.amount * self.quantity | ||||
|         if not created and not self.valid and not hasattr(self, "_force_save"): | ||||
|         to_transfer = self.total | ||||
|         if not created: | ||||
|             # Revert old transaction | ||||
|             old_transaction = Transaction.objects.get(pk=self.pk) | ||||
|             # We make a select for update to avoid concurrency issues | ||||
|             old_transaction = Transaction.objects.select_for_update().get(pk=self.pk) | ||||
|             # Check that nothing important changed | ||||
|             for field_name in ["source_id", "destination_id", "quantity", "amount"]: | ||||
|                 if getattr(self, field_name) != getattr(old_transaction, field_name): | ||||
|                     raise ValidationError(_("You can't update the {field} on a Transaction. " | ||||
|                                             "Please invalidate it and create one other.").format(field=field_name)) | ||||
|             if not hasattr(self, "_force_save"): | ||||
|                 for field_name in ["source_id", "destination_id", "quantity", "amount"]: | ||||
|                     if getattr(self, field_name) != getattr(old_transaction, field_name): | ||||
|                         raise ValidationError(_("You can't update the {field} on a Transaction. " | ||||
|                                                 "Please invalidate it and create one other.").format(field=field_name)) | ||||
|  | ||||
|             if old_transaction.valid == self.valid: | ||||
|                 # Don't change anything | ||||
| @@ -215,9 +217,8 @@ class Transaction(PolymorphicModel): | ||||
|             # When source == destination, no money is transferred and no transaction is created | ||||
|             return | ||||
|  | ||||
|         # We refresh the notes with the "select for update" tag to avoid concurrency issues | ||||
|         self.source = Note.objects.filter(pk=self.source_id).select_for_update().get() | ||||
|         self.destination = Note.objects.filter(pk=self.destination_id).select_for_update().get() | ||||
|         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() | ||||
| @@ -237,9 +238,11 @@ class Transaction(PolymorphicModel): | ||||
|         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() | ||||
| @@ -273,6 +276,7 @@ class RecurrentTransaction(Transaction): | ||||
|                 _("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) | ||||
| @@ -323,6 +327,7 @@ class SpecialTransaction(Transaction): | ||||
|             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) | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.utils import timezone | ||||
|  | ||||
|  | ||||
| def save_user_note(instance, raw, **_kwargs): | ||||
|     """ | ||||
| @@ -25,6 +27,16 @@ def save_club_note(instance, raw, **_kwargs): | ||||
|         instance.note.save() | ||||
|  | ||||
|  | ||||
| def pre_save_note(instance, raw, **_kwargs): | ||||
|     if not raw and instance.pk and not hasattr(instance, "_no_signal") and instance.balance < 0: | ||||
|         from note.models import Note | ||||
|         old_note = Note.objects.get(pk=instance.pk) | ||||
|         if old_note.balance >= 0: | ||||
|             # Passage en négatif | ||||
|             instance.last_negative = timezone.now() | ||||
|             instance.send_mail_negative_balance() | ||||
|  | ||||
|  | ||||
| def delete_transaction(instance, **_kwargs): | ||||
|     """ | ||||
|     Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first. | ||||
|   | ||||
| @@ -29,7 +29,6 @@ $(document).ready(function () { | ||||
|   // Switching in double consumptions mode should update the layout
 | ||||
|   $('#double_conso').change(function () { | ||||
|     $('#consos_list_div').removeClass('d-none') | ||||
|     $('#user_select_div').attr('class', 'col-xl-4') | ||||
|     $('#infos_div').attr('class', 'col-sm-5 col-xl-6') | ||||
| 
 | ||||
|     const note_list_obj = $('#note_list') | ||||
| @@ -48,7 +47,6 @@ $(document).ready(function () { | ||||
| 
 | ||||
|   $('#single_conso').change(function () { | ||||
|     $('#consos_list_div').addClass('d-none') | ||||
|     $('#user_select_div').attr('class', 'col-xl-7') | ||||
|     $('#infos_div').attr('class', 'col-sm-5 col-md-4') | ||||
| 
 | ||||
|     const consos_list_obj = $('#consos_list') | ||||
| @@ -224,17 +222,14 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca | ||||
|       if (!isNaN(source.balance)) { | ||||
|         const newBalance = source.balance - quantity * amount | ||||
|         if (newBalance <= -5000) { | ||||
|           addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + | ||||
|                         'succès, mais la note émettrice ' + source_alias + ' est en négatif sévère.', | ||||
|           'danger', 30000) | ||||
|           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) { | ||||
|           addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + | ||||
|                         'succès, mais la note émettrice ' + source_alias + ' est en négatif.', | ||||
|           'warning', 30000) | ||||
|           addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + | ||||
|               'but the emitter note %s is negative.', [source_alias, source_alias])), 'warning', 30000) | ||||
|         } | ||||
|         if (source.membership && source.membership.date_end < new Date().toISOString()) { | ||||
|           addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", | ||||
|             'danger', 30000) | ||||
|           addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.', [source_alias])), 'danger', 30000) | ||||
|         } | ||||
|       } | ||||
|       reset() | ||||
| @@ -255,7 +250,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca | ||||
|           template: template | ||||
|         }).done(function () { | ||||
|         reset() | ||||
|         addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000) | ||||
|         addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000) | ||||
|       }).fail(function () { | ||||
|         reset() | ||||
|         errMsg(e.responseJSON) | ||||
| @@ -67,7 +67,11 @@ $(document).ready(function () { | ||||
| 
 | ||||
|       last.quantity = 1 | ||||
| 
 | ||||
|       if (!last.note.user) { | ||||
|       if (last.note.club) { | ||||
|         $('#last_name').val(last.note.name) | ||||
|         $('#first_name').val(last.note.name) | ||||
|       } | ||||
|       else if (!last.note.user) { | ||||
|         $.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) { | ||||
|           last.note.user = note.user | ||||
|           $.getJSON('/api/user/' + last.note.user + '/', function (user) { | ||||
| @@ -235,20 +239,20 @@ $('#btn_transfer').click(function () { | ||||
| 
 | ||||
|   if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { | ||||
|     amount_field.addClass('is-invalid') | ||||
|     $('#amount-required').html('<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>') | ||||
|     $('#amount-required').html('<strong>' + gettext('This field is required and must contain a decimal positive number.') + '</strong>') | ||||
|     error = true | ||||
|   } | ||||
| 
 | ||||
|   const amount = Math.floor(100 * amount_field.val()) | ||||
|   if (amount > 2147483647) { | ||||
|     amount_field.addClass('is-invalid') | ||||
|     $('#amount-required').html('<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>') | ||||
|     $('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>') | ||||
|     error = true | ||||
|   } | ||||
| 
 | ||||
|   if (!reason_field.val()) { | ||||
|   if (!reason_field.val() && $('#type_transfer').is(':checked')) { | ||||
|     reason_field.addClass('is-invalid') | ||||
|     $('#reason-required').html('<strong>Ce champ est requis.</strong>') | ||||
|     $('#reason-required').html('<strong>' + gettext('This field is required.') + '</strong>') | ||||
|     error = true | ||||
|   } | ||||
| 
 | ||||
| @@ -274,9 +278,8 @@ $('#btn_transfer').click(function () { | ||||
|     [...sources_notes_display].forEach(function (source) { | ||||
|       [...dests_notes_display].forEach(function (dest) { | ||||
|         if (source.note.id === dest.note.id) { | ||||
|           addMsg('Attention : la transaction de ' + pretty_money(amount) + ' de la note ' + source.name + | ||||
|                         ' vers la note ' + dest.name + " n'a pas été faite car il s'agit de la même note au départ" + | ||||
|                         " et à l'arrivée.", 'warning', 10000) | ||||
|           addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' + | ||||
|               'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000) | ||||
|           LOCK = false | ||||
|           return | ||||
|         } | ||||
| @@ -296,43 +299,35 @@ $('#btn_transfer').click(function () { | ||||
|             destination_alias: dest.name | ||||
|           }).done(function () { | ||||
|           if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { | ||||
|             addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", | ||||
|               'danger', 30000) | ||||
|             addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000) | ||||
|           } | ||||
|           if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { | ||||
|             addMsg('Attention : la note destination ' + dest.name + " n'est plus adhérente.", | ||||
|               'danger', 30000) | ||||
|             addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [source.name]), 'danger', 30000) | ||||
|           } | ||||
| 
 | ||||
|           if (!isNaN(source.note.balance)) { | ||||
|             const newBalance = source.note.balance - source.quantity * dest.quantity * amount | ||||
|             if (newBalance <= -5000) { | ||||
|               addMsg('Le transfert de ' + | ||||
|                                     pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + | ||||
|                                     source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' + | ||||
|                                     'mais la note émettrice est en négatif sévère.', 'danger', 10000) | ||||
|               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() | ||||
|               return | ||||
|             } else if (newBalance < 0) { | ||||
|               addMsg('Le transfert de ' + | ||||
|                                     pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + | ||||
|                                     source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' + | ||||
|                                     'mais la note émettrice est en négatif.', 'warning', 10000) | ||||
|               addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'), | ||||
|                   [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000) | ||||
|               reset() | ||||
|               return | ||||
|             } | ||||
|           } | ||||
|           addMsg('Le transfert de ' + | ||||
|                             pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + | ||||
|                             ' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000) | ||||
|           addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'), | ||||
|               [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000) | ||||
| 
 | ||||
|           reset() | ||||
|         }).fail(function (err) { // do it again but valid = false
 | ||||
|           const errObj = JSON.parse(err.responseText) | ||||
|           if (errObj.non_field_errors) { | ||||
|             addMsg('Le transfert de ' + | ||||
|                                 pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + | ||||
|                                 ' vers la note ' + dest.name + ' a échoué : ' + errObj.non_field_errors, 'danger') | ||||
|             addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), | ||||
|                 [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger') | ||||
|             LOCK = false | ||||
|             return | ||||
|           } | ||||
| @@ -352,17 +347,15 @@ $('#btn_transfer').click(function () { | ||||
|               destination: dest.note.id, | ||||
|               destination_alias: dest.name | ||||
|             }).done(function () { | ||||
|             addMsg('Le transfert de ' + | ||||
|                                 pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + | ||||
|                                 ' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000) | ||||
|             addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), | ||||
|                 [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000) | ||||
|             reset() | ||||
|           }).fail(function (err) { | ||||
|             const errObj = JSON.parse(err.responseText) | ||||
|             let error = errObj.detail ? errObj.detail : errObj.non_field_errors | ||||
|             if (!error) { error = err.responseText } | ||||
|             addMsg('Le transfert de ' + | ||||
|                                 pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + | ||||
|                                 ' vers la note ' + dest.name + ' a échoué : ' + error, 'danger') | ||||
|             addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), | ||||
|                 [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger') | ||||
|             LOCK = false | ||||
|           }) | ||||
|         }) | ||||
| @@ -388,7 +381,7 @@ $('#btn_transfer').click(function () { | ||||
|       alias = sources_notes_display[0].name | ||||
|       source_id = user_note.id | ||||
|       dest_id = special_note | ||||
|       reason = 'Retrait ' + $('#credit_type option:selected').text().toLowerCase() | ||||
|       reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase() | ||||
|       if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } | ||||
|     } | ||||
|     $.post('/api/note/transaction/transaction/', | ||||
| @@ -408,14 +401,14 @@ $('#btn_transfer').click(function () { | ||||
|         first_name: $('#first_name').val(), | ||||
|         bank: $('#bank').val() | ||||
|       }).done(function () { | ||||
|       addMsg('Le crédit/retrait a bien été effectué !', 'success', 10000) | ||||
|       if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg('Attention : la note ' + alias + " n'est plus adhérente.", 'danger', 10000) } | ||||
|       addMsg(gettext('Credit/debit succeed!'), 'success', 10000) | ||||
|       if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } | ||||
|       reset() | ||||
|     }).fail(function (err) { | ||||
|       const errObj = JSON.parse(err.responseText) | ||||
|       let error = errObj.detail ? errObj.detail : errObj.non_field_errors | ||||
|       if (!error) { error = err.responseText } | ||||
|       addMsg('Le crédit/retrait a échoué : ' + error, 'danger', 10000) | ||||
|       addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000) | ||||
|       LOCK = false | ||||
|     }) | ||||
|   } | ||||
| @@ -10,22 +10,22 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% block content %} | ||||
|     <div class="row mt-4"> | ||||
|         <div class="col-sm-5 col-md-4" id="infos_div"> | ||||
|             <div class="row"> | ||||
|             <div class="row justify-content-center justify-content-md-end"> | ||||
|                 {# User details column #} | ||||
|                 <div class="col"> | ||||
|                     <div class="card bg-light border-success mb-4 text-center"> | ||||
|                 <div class="col picture-col"> | ||||
|                     <div class="card bg-light mb-4 text-center"> | ||||
|                         <a id="profile_pic_link" href="#"> | ||||
|                             <img src="{% static "member/img/default_picture.png" %}" | ||||
|                                  id="profile_pic" alt="" class="card-img-top"> | ||||
|                                  id="profile_pic" alt="" class="card-img-top d-none d-sm-block"> | ||||
|                         </a> | ||||
|                         <div class="card-body text-center text-break"> | ||||
|                             <span id="user_note"></span> | ||||
|                         <div class="card-body text-center text-break p-2"> | ||||
|                             <span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 {# User selection column #} | ||||
|                 <div class="col-xl-7" id="user_select_div"> | ||||
|                 <div class="col-xl" id="user_select_div"> | ||||
|                     <div class="card bg-light border-success mb-4"> | ||||
|                         <div class="card-header"> | ||||
|                             <p class="card-text font-weight-bold"> | ||||
| @@ -44,6 +44,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 {# Summary of consumption and consume button #} | ||||
|                 <div class="col-xl-5 d-none" id="consos_list_div"> | ||||
|                     <div class="card bg-light border-info mb-4"> | ||||
| @@ -65,7 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|             {# Show last used buttons #} | ||||
|             <div class="card bg-light mb-4"> | ||||
|                 <div class="card-header"> | ||||
| @@ -159,7 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script type="text/javascript" src="{% static "js/consos.js" %}"></script> | ||||
|     <script type="text/javascript" src="{% static "note/js/consos.js" %}"></script> | ||||
|     <script type="text/javascript"> | ||||
|         {% for button in highlighted %} | ||||
|             {% if button.display %} | ||||
|   | ||||
| @@ -34,21 +34,21 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|         </div> | ||||
|     </div> | ||||
|     <hr> | ||||
|     <div class="row"> | ||||
|     <div class="row justify-content-center"> | ||||
|         {#  Preview note profile (picture, username and balance) #} | ||||
|         <div class="col-md-3" id="note_infos_div"> | ||||
|             <div class="card bg-light border-success shadow mb-4 pt-4 text-center"> | ||||
|         <div class="col-md picture-col" id="note_infos_div"> | ||||
|             <div class="card bg-light mb-4 text-center"> | ||||
|                 <a id="profile_pic_link" href="#"><img src="{% static "member/img/default_picture.png" %}" | ||||
|                         id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a> | ||||
|                 <div class="card-body text-center"> | ||||
|                     <span id="user_note"></span> | ||||
|                 <div class="card-body text-center p-2"> | ||||
|                     <span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         {# list of emitters #} | ||||
|         <div class="col-md-3" id="emitters_div"> | ||||
|             <div class="card bg-light border-success shadow mb-4"> | ||||
|             <div class="card bg-light mb-4"> | ||||
|                 <div class="card-header"> | ||||
|                     <p class="card-text font-weight-bold"> | ||||
|                         <label for="source_note" id="source_note_label">{% trans "Select emitters" %}</label> | ||||
| @@ -75,7 +75,7 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|  | ||||
|         {# list of receiver #} | ||||
|         <div class="col-md-3" id="dests_div"> | ||||
|             <div class="card bg-light border-info shadow mb-4"> | ||||
|             <div class="card bg-light mb-4"> | ||||
|                 <div class="card-header"> | ||||
|                     <p class="card-text font-weight-bold" id="dest_title"> | ||||
|                         <label for="dest_note" id="dest_note_label">{% trans "Select receivers" %}</label> | ||||
| @@ -97,8 +97,8 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|         </div> | ||||
|  | ||||
|         {# Information on transaction (amount, reason, name,...) #} | ||||
|         <div class="col-md-3" id="external_div"> | ||||
|             <div class="card bg-light border-warning shadow mb-4"> | ||||
|         <div class="col-md" id="external_div"> | ||||
|             <div class="card bg-light mb-4"> | ||||
|                 <div class="card-header"> | ||||
|                     <p class="card-text font-weight-bold"> | ||||
|                         {% trans "Action" %} | ||||
| @@ -153,7 +153,7 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|         </div> | ||||
|     </div> | ||||
| {# transaction history #} | ||||
|     <div class="card shadow mb-4" id="history"> | ||||
|     <div class="card mb-4" id="history"> | ||||
|         <div class="card-header"> | ||||
|             <p class="card-text font-weight-bold"> | ||||
|                 {% trans "Recent transactions history" %} | ||||
| @@ -176,5 +176,5 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|         select_receveirs_label = "{% trans "Select receivers" %}"; | ||||
|         transfer_type_label = "{% trans "Transfer type" %}"; | ||||
|     </script> | ||||
|     <script src="/static/js/transfer.js"></script> | ||||
|     <script src="{% static "note/js/transfer.js" %}"></script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,15 +1,20 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from api.tests import TestAPI | ||||
| from member.models import Club, Membership | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
| from member.models import Club, Membership | ||||
| from note.models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \ | ||||
|     MembershipTransaction, SpecialTransaction, NoteSpecial, Alias | ||||
| from django.utils import timezone | ||||
| from permission.models import Role | ||||
|  | ||||
| from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\ | ||||
|     TransactionTemplateViewSet, TransactionViewSet | ||||
| from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \ | ||||
|     MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note | ||||
|  | ||||
|  | ||||
| class TestTransactions(TestCase): | ||||
|     fixtures = ('initial', ) | ||||
| @@ -297,8 +302,8 @@ class TestTransactions(TestCase): | ||||
|  | ||||
|     def test_render_search_transactions(self): | ||||
|         response = self.client.get(reverse("note:transactions", args=(self.user.note.pk,)), data=dict( | ||||
|             source=self.second_user.note.alias_set.first().id, | ||||
|             destination=self.user.note.alias_set.first().id, | ||||
|             source=self.second_user.note.alias.first().id, | ||||
|             destination=self.user.note.alias.first().id, | ||||
|             type=[ContentType.objects.get_for_model(Transaction).id], | ||||
|             reason="test", | ||||
|             valid=True, | ||||
| @@ -363,3 +368,69 @@ class TestTransactions(TestCase): | ||||
|         self.assertTrue(Alias.objects.filter(name="test_updated_alias").exists()) | ||||
|         response = self.client.delete("/api/note/alias/" + str(alias.pk) + "/") | ||||
|         self.assertEqual(response.status_code, 204) | ||||
|  | ||||
|  | ||||
| class TestNoteAPI(TestAPI): | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|  | ||||
|         membership = Membership.objects.create(club=Club.objects.get(name="BDE"), user=self.user) | ||||
|         membership.roles.add(Role.objects.get(name="Respo info")) | ||||
|         membership.save() | ||||
|         Membership.objects.create(club=Club.objects.get(name="Kfet"), user=self.user) | ||||
|         self.user.note.last_negative = timezone.now() | ||||
|         self.user.note.save() | ||||
|  | ||||
|         self.transaction = Transaction.objects.create( | ||||
|             source=Note.objects.first(), | ||||
|             destination=self.user.note, | ||||
|             amount=4200, | ||||
|             reason="Test transaction", | ||||
|         ) | ||||
|         self.user.note.refresh_from_db() | ||||
|         Alias.objects.create(note=self.user.note, name="I am a ¢omplex alias") | ||||
|  | ||||
|         self.category = TemplateCategory.objects.create(name="Test") | ||||
|         self.template = TransactionTemplate.objects.create( | ||||
|             name="Test", | ||||
|             destination=Club.objects.get(name="BDE").note, | ||||
|             category=self.category, | ||||
|             amount=100, | ||||
|             description="Test template", | ||||
|         ) | ||||
|  | ||||
|     def test_alias_api(self): | ||||
|         """ | ||||
|         Load Alias API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(AliasViewSet, "/api/note/alias/") | ||||
|  | ||||
|     def test_consumer_api(self): | ||||
|         """ | ||||
|         Load Consumer API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ConsumerViewSet, "/api/note/consumer/") | ||||
|  | ||||
|     def test_note_api(self): | ||||
|         """ | ||||
|         Load Note API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(NotePolymorphicViewSet, "/api/note/note/") | ||||
|  | ||||
|     def test_template_category_api(self): | ||||
|         """ | ||||
|         Load TemplateCategory API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(TemplateCategoryViewSet, "/api/note/transaction/category/") | ||||
|  | ||||
|     def test_transaction_template_api(self): | ||||
|         """ | ||||
|         Load TemplateTemplate API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(TransactionTemplateViewSet, "/api/note/transaction/template/") | ||||
|  | ||||
|     def test_transaction_api(self): | ||||
|         """ | ||||
|         Load Transaction API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(TransactionViewSet, "/api/note/transaction/transaction/") | ||||
|   | ||||
| @@ -144,7 +144,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up | ||||
| class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     The Magic View that make people pay their beer and burgers. | ||||
|     (Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`) | ||||
|     (Most of the magic happens in the dark world of Javascript see `static/note/js/consos.js`) | ||||
|     """ | ||||
|     model = Transaction | ||||
|     template_name = "note/conso_form.html" | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from api.viewsets import ReadOnlyProtectedModelViewSet | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework.filters import SearchFilter | ||||
|  | ||||
| from .serializers import PermissionSerializer, RoleSerializer | ||||
| from ..models import Permission, Role | ||||
| @@ -14,10 +15,11 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/permission/permission/ | ||||
|     """ | ||||
|     queryset = Permission.objects.all() | ||||
|     queryset = Permission.objects.order_by('id') | ||||
|     serializer_class = PermissionSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['model', 'type', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ] | ||||
|     search_fields = ['$model__name', '$query', '$description', ] | ||||
|  | ||||
|  | ||||
| class RoleViewSet(ReadOnlyProtectedModelViewSet): | ||||
| @@ -26,7 +28,8 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer | ||||
|     then render it on /api/permission/roles/ | ||||
|     """ | ||||
|     queryset = Role.objects.all() | ||||
|     queryset = Role.objects.order_by('id') | ||||
|     serializer_class = RoleSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['role', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ] | ||||
|     SearchFilter = ['$name', '$for_club__name', ] | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import sys | ||||
| from functools import lru_cache | ||||
| from time import time | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.sessions.models import Session | ||||
| from note_kfet.middlewares import get_current_session | ||||
|  | ||||
| @@ -33,12 +32,16 @@ def memoize(f): | ||||
|         sess_funs = new_sess_funs | ||||
|  | ||||
|     def func(*args, **kwargs): | ||||
|         if settings.DEBUG: | ||||
|             # Don't memoize in DEBUG mode | ||||
|             return f(*args, **kwargs) | ||||
|         # if settings.DEBUG: | ||||
|         #     # Don't memoize in DEBUG mode | ||||
|         #     return f(*args, **kwargs) | ||||
|  | ||||
|         nonlocal last_collect | ||||
|  | ||||
|         if "test" in sys.argv: | ||||
|             # In a test environment, don't memoize permissions | ||||
|             return f(*args, **kwargs) | ||||
|  | ||||
|         if time() - last_collect > 60: | ||||
|             # Clear cache | ||||
|             collect() | ||||
|   | ||||
| @@ -115,7 +115,7 @@ | ||||
| 			"type": "view", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": true, | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir les aliases des notes des clubs et des adhérents du club Kfet" | ||||
| 		} | ||||
| 	}, | ||||
| @@ -799,12 +799,12 @@ | ||||
| 				"member", | ||||
| 				"membership" | ||||
| 			], | ||||
| 			"query": "{\"club\": [\"club\"]}", | ||||
| 			"query": "{}", | ||||
| 			"type": "change", | ||||
| 			"mask": 3, | ||||
| 			"field": "roles", | ||||
| 			"permanent": false, | ||||
| 			"description": "Modifier les rôles d'un adhérent d'un club" | ||||
| 			"description": "Modifier les rôles d'une adhésion" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| @@ -819,7 +819,7 @@ | ||||
| 			"type": "change", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"permanent": true, | ||||
| 			"description": "Modifier son profil" | ||||
| 		} | ||||
| 	}, | ||||
| @@ -1103,7 +1103,7 @@ | ||||
| 				"treasury", | ||||
| 				"sogecredit" | ||||
| 			], | ||||
| 			"query": "{\"credit_transaction\": null}", | ||||
| 			"query": "{}", | ||||
| 			"type": "add", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| @@ -2081,7 +2081,7 @@ | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "change", | ||||
| 			"mask": 1, | ||||
| 			"mask": 2, | ||||
| 			"field": "invalidity_reason", | ||||
| 			"permanent": false, | ||||
| 			"description": "Modifier la raison d'invalidité d'une transaction" | ||||
| @@ -2647,6 +2647,230 @@ | ||||
| 			"description": "Changer l'image de la note de son club" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 170, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"note", | ||||
| 				"alias" | ||||
| 			], | ||||
| 			"query": "{\"note__is_active\": true}", | ||||
| 			"type": "add", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Ajouter n'importe quel alias à une note non bloquée" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 171, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"note", | ||||
| 				"alias" | ||||
| 			], | ||||
| 			"query": "{\"note__is_active\": true}", | ||||
| 			"type": "delete", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Supprimer n'importe quel alias à une note non bloquée" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 172, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"treasury", | ||||
| 				"remittance" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "view", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir toutes les remises" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 173, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"treasury", | ||||
| 				"remittance" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "add", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Ajouter une remise" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 174, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"treasury", | ||||
| 				"remittance" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "change", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Modifier une remise" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 175, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"treasury", | ||||
| 				"remittance" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "delete", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Supprimer une remise" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 176, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"auth", | ||||
| 				"user" | ||||
| 			], | ||||
| 			"query": "{\"profile__registration_valid\": false}", | ||||
| 			"type": "change", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Modifier n'importe quel utilisateur non encore inscrit" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 177, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"member", | ||||
| 				"profile" | ||||
| 			], | ||||
| 			"query": "{\"registration_valid\": false}", | ||||
| 			"type": "change", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Modifier n'importe quel profil non encore inscrit" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 178, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"note", | ||||
| 				"alias" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "view", | ||||
| 			"mask": 3, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir tous les alias, y compris ceux des non adhérents" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 179, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"note", | ||||
| 				"alias" | ||||
| 			], | ||||
| 			"query": "{\"note__noteuser__user\": [\"user\"]}", | ||||
| 			"type": "view", | ||||
| 			"mask": 1, | ||||
| 			"field": "", | ||||
| 			"permanent": true, | ||||
| 			"description": "Voir ses propres alias, pour toujours" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 180, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"auth", | ||||
| 				"user" | ||||
| 			], | ||||
| 			"query": "{\"profile__registration_valid\": false}", | ||||
| 			"type": "view", | ||||
| 			"mask": 2, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir n'importe quel utilisateur non encore inscrit" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 181, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"member", | ||||
| 				"profile" | ||||
| 			], | ||||
| 			"query": "{\"registration_valid\": false}", | ||||
| 			"type": "view", | ||||
| 			"mask": 2, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir n'importe quel profil non encore inscrit" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 182, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"auth", | ||||
| 				"user" | ||||
| 			], | ||||
| 			"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}", | ||||
| 			"type": "view", | ||||
| 			"mask": 2, | ||||
| 			"field": "", | ||||
| 			"permanent": false, | ||||
| 			"description": "Voir n'importe quel utilisateur qui est adhérent BDE" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.permission", | ||||
| 		"pk": 183, | ||||
| 		"fields": { | ||||
| 			"model": [ | ||||
| 				"note", | ||||
| 				"note" | ||||
| 			], | ||||
| 			"query": "{}", | ||||
| 			"type": "change", | ||||
| 			"mask": 1, | ||||
| 			"field": "display_image", | ||||
| 			"permanent": false, | ||||
| 			"description": "Changer l'image de n'importe quelle note" | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.role", | ||||
| 		"pk": 1, | ||||
| @@ -2717,7 +2941,8 @@ | ||||
| 				157, | ||||
| 				158, | ||||
| 				159, | ||||
| 				160 | ||||
| 				160, | ||||
| 				179 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| @@ -2778,14 +3003,14 @@ | ||||
| 				62, | ||||
| 				127, | ||||
| 				133, | ||||
| 				135, | ||||
| 				136, | ||||
| 				141, | ||||
| 				142, | ||||
| 				150, | ||||
| 				166, | ||||
| 				167, | ||||
| 				168 | ||||
| 				168, | ||||
| 				182 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| @@ -2821,6 +3046,7 @@ | ||||
| 				31, | ||||
| 				32, | ||||
| 				33, | ||||
| 				51, | ||||
| 				53, | ||||
| 				54, | ||||
| 				55, | ||||
| @@ -2844,13 +3070,24 @@ | ||||
| 				137, | ||||
| 				138, | ||||
| 				139, | ||||
| 				140, | ||||
| 				143, | ||||
| 				146, | ||||
| 				147, | ||||
| 				150, | ||||
| 				151, | ||||
| 				163, | ||||
| 				164 | ||||
| 				164, | ||||
| 				170, | ||||
| 				171, | ||||
| 				172, | ||||
| 				173, | ||||
| 				174, | ||||
| 				175, | ||||
| 				176, | ||||
| 				177, | ||||
| 				178, | ||||
| 				183 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| @@ -3024,7 +3261,21 @@ | ||||
| 				166, | ||||
| 				167, | ||||
| 				168, | ||||
| 				169 | ||||
| 				169, | ||||
| 				170, | ||||
| 				171, | ||||
| 				172, | ||||
| 				173, | ||||
| 				174, | ||||
| 				175, | ||||
| 				176, | ||||
| 				177, | ||||
| 				178, | ||||
| 				179, | ||||
| 				180, | ||||
| 				181, | ||||
| 				182, | ||||
| 				183 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| @@ -3050,10 +3301,20 @@ | ||||
| 				29, | ||||
| 				30, | ||||
| 				31, | ||||
| 				70, | ||||
| 				143, | ||||
| 				166, | ||||
| 				167, | ||||
| 				168 | ||||
| 				168, | ||||
| 				170, | ||||
| 				171, | ||||
| 				176, | ||||
| 				177, | ||||
| 				178, | ||||
| 				179, | ||||
| 				180, | ||||
| 				181, | ||||
| 				182 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| @@ -3216,13 +3477,50 @@ | ||||
| 				135, | ||||
| 				136, | ||||
| 				137, | ||||
| 				138, | ||||
| 				139, | ||||
| 				140, | ||||
| 				143, | ||||
| 				145, | ||||
| 				146, | ||||
| 				147, | ||||
| 				150 | ||||
| 				150, | ||||
| 				176, | ||||
| 				177 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
| 	{ | ||||
| 		"model": "permission.role", | ||||
| 		"pk": 20, | ||||
| 		"fields": { | ||||
| 			"for_club": 2, | ||||
| 			"name": "PC Kfet", | ||||
| 			"permissions": [ | ||||
| 				6, | ||||
| 				22, | ||||
| 				24, | ||||
| 				25, | ||||
| 				26, | ||||
| 				27, | ||||
| 				30, | ||||
| 				49, | ||||
| 				50, | ||||
| 				55, | ||||
| 				56, | ||||
| 				57, | ||||
| 				58, | ||||
| 				137, | ||||
| 				143, | ||||
| 				147, | ||||
| 				150, | ||||
| 				166, | ||||
| 				167, | ||||
| 				168, | ||||
| 				176, | ||||
| 				177, | ||||
| 				180, | ||||
| 				181, | ||||
| 				182 | ||||
| 			] | ||||
| 		} | ||||
| 	}, | ||||
|   | ||||
| @@ -43,7 +43,9 @@ class InstancedPermission: | ||||
|                 obj = copy(obj) | ||||
|                 obj.pk = 0 | ||||
|                 with transaction.atomic(): | ||||
|                     sid = transaction.savepoint() | ||||
|                     for o in self.model.model_class().objects.filter(pk=0).all(): | ||||
|                         o._no_signal = True | ||||
|                         o._force_delete = True | ||||
|                         Model.delete(o) | ||||
|                         # An object with pk 0 wouldn't deleted. That's not normal, we alert admins. | ||||
| @@ -61,9 +63,7 @@ class InstancedPermission: | ||||
|                     obj._no_signal = True | ||||
|                     Model.save(obj, force_insert=True) | ||||
|                     ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists() | ||||
|                     # Delete testing object | ||||
|                     obj._force_delete = True | ||||
|                     Model.delete(obj) | ||||
|                     transaction.savepoint_rollback(sid) | ||||
|  | ||||
|                 return ret | ||||
|  | ||||
| @@ -199,6 +199,7 @@ class Permission(models.Model): | ||||
|         if self.field and self.type not in {'view', 'change'}: | ||||
|             raise ValidationError(_("Specifying field applies only to view and change permission types.")) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, **kwargs): | ||||
|         self.full_clean() | ||||
|         super().save() | ||||
|   | ||||
| @@ -14,6 +14,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): | ||||
|     This is a simple patch of this class that controls view access. | ||||
|     """ | ||||
|  | ||||
|     # The queryset is filtered, and permissions are more powerful than a simple check than just "can view this model" | ||||
|     perms_map = { | ||||
|         'GET': ['%(app_label)s.view_%(model_name)s'], | ||||
|         'OPTIONS': [], | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from django.contrib.auth.models import AnonymousUser | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.template.defaultfilters import stringfilter | ||||
| from django import template | ||||
| from note.models import Transaction | ||||
| from note_kfet.middlewares import get_current_authenticated_user, get_current_session | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| @@ -25,21 +24,6 @@ def not_empty_model_list(model_name): | ||||
|     return qs.exists() | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| def not_empty_model_change_list(model_name): | ||||
|     """ | ||||
|     Return True if and only if the current user has right to change any object of the given model. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     session = get_current_session() | ||||
|     if user is None or isinstance(user, AnonymousUser): | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", -1) >= 42: | ||||
|         return True | ||||
|     qs = model_list(model_name, "change") | ||||
|     return qs.exists() | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| def model_list(model_name, t="view", fetch=True): | ||||
|     """ | ||||
| @@ -68,33 +52,8 @@ def has_perm(perm, obj): | ||||
|     return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj) | ||||
|  | ||||
|  | ||||
| def can_create_transaction(): | ||||
|     """ | ||||
|     :return: True iff the authenticated user can create a transaction. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     session = get_current_session() | ||||
|     if user is None or isinstance(user, AnonymousUser): | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", -1) >= 42: | ||||
|         return True | ||||
|     if session.get("can_create_transaction", None): | ||||
|         return session.get("can_create_transaction", None) == 1 | ||||
|  | ||||
|     empty_transaction = Transaction( | ||||
|         source=user.note, | ||||
|         destination=user.note, | ||||
|         quantity=1, | ||||
|         amount=0, | ||||
|         reason="Check permissions", | ||||
|     ) | ||||
|     session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction) | ||||
|     return session.get("can_create_transaction") == 1 | ||||
|  | ||||
|  | ||||
| register = template.Library() | ||||
| register.filter('not_empty_model_list', not_empty_model_list) | ||||
| register.filter('not_empty_model_change_list', not_empty_model_change_list) | ||||
| register.filter('model_list', model_list) | ||||
| register.filter('model_list_length', model_list_length) | ||||
| register.filter('has_perm', has_perm) | ||||
|   | ||||
| @@ -78,7 +78,7 @@ class PermissionQueryTestCase(TestCase): | ||||
|                 query = instanced.query | ||||
|                 model = perm.model.model_class() | ||||
|                 model.objects.filter(query).all() | ||||
|             except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError): | ||||
|             except (FieldError, AttributeError, ValueError, TypeError, JSONDecodeError):  # pragma: no cover | ||||
|                 print("Query error for permission", perm) | ||||
|                 print("Query:", perm.query) | ||||
|                 if instanced.query: | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from datetime import date | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.db import transaction | ||||
| from django.db.models import Q | ||||
| from django.forms import HiddenInput | ||||
| from django.http import Http404 | ||||
| @@ -50,12 +51,15 @@ class ProtectQuerysetMixin: | ||||
|         # No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make | ||||
|         # a custom request. | ||||
|         # We could also delete the field, but some views might be affected. | ||||
|         meta = form.instance._meta | ||||
|         for key in form.base_fields: | ||||
|             if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object): | ||||
|             if not PermissionBackend.check_perm(self.request.user, | ||||
|                                                 f"{meta.app_label}.change_{meta.model_name}_" + key, self.object): | ||||
|                 form.fields[key].widget = HiddenInput() | ||||
|  | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Submit the form, if the page is a FormView. | ||||
| @@ -81,7 +85,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView): | ||||
|     If not, a 403 error is displayed. | ||||
|     """ | ||||
|  | ||||
|     def get_sample_object(self): | ||||
|     def get_sample_object(self):  # pragma: no cover | ||||
|         """ | ||||
|         return a sample instance of the Model. | ||||
|         It should be valid (can be stored properly in database), but must not collide with existing data. | ||||
|   | ||||
| @@ -44,6 +44,15 @@ class SignUpForm(UserCreationForm): | ||||
|         fields = ('first_name', 'last_name', 'username', 'email', ) | ||||
|  | ||||
|  | ||||
| class DeclareSogeAccountOpenedForm(forms.Form): | ||||
|     soge_account = forms.BooleanField( | ||||
|         label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."), | ||||
|         help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your " | ||||
|                     "account, you will have to pay the BDE membership."), | ||||
|         required=False, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class WEISignupForm(forms.Form): | ||||
|     wei_registration = forms.BooleanField( | ||||
|         label=_("Register to the WEI"), | ||||
| @@ -60,7 +69,7 @@ class ValidationForm(forms.Form): | ||||
|     soge = forms.BooleanField( | ||||
|         label=_("Inscription paid by Société Générale"), | ||||
|         required=False, | ||||
|         help_text=_("Check this case is the Société Générale paid the inscription."), | ||||
|         help_text=_("Check this case if the Société Générale paid the inscription."), | ||||
|     ) | ||||
|  | ||||
|     credit_type = forms.ModelChoiceField( | ||||
|   | ||||
| @@ -4,6 +4,8 @@ | ||||
| import django_tables2 as tables | ||||
| from django.contrib.auth.models import User | ||||
|  | ||||
| from treasury.models import SogeCredit | ||||
|  | ||||
|  | ||||
| class FutureUserTable(tables.Table): | ||||
|     """ | ||||
| @@ -21,6 +23,7 @@ class FutureUserTable(tables.Table): | ||||
|         fields = ('last_name', 'first_name', 'username', 'email', ) | ||||
|         model = User | ||||
|         row_attrs = { | ||||
|             'class': 'table-row', | ||||
|             'class': lambda record: 'table-row' | ||||
|                                     + (' bg-warning' if SogeCredit.objects.filter(user=record).exists() else ''), | ||||
|             'data-href': lambda record: record.pk | ||||
|         } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="row mt-4"> | ||||
|         <div class="col-md-3 mb-4"> | ||||
|         <div class="col-xl-5 mb-4"> | ||||
|             <div class="card bg-light shadow"> | ||||
|                 <div class="card-header text-center" > | ||||
|                     <h4> {% trans "Account #" %}  {{ object.pk }}</h4> | ||||
| @@ -50,12 +50,19 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="col-md-9"> | ||||
|         <div class="col-md-7"> | ||||
|             <div class="card bg-light shadow"> | ||||
|                 <form method="post"> | ||||
|                     <div class="card-header text-center" > | ||||
|                         <h4> {% trans "Validate account" %}</h4> | ||||
|                     </div> | ||||
|  | ||||
|                     {% if declare_soge_account %} | ||||
|                         <div class="alert alert-info"> | ||||
|                         {% trans "The user declared that he/she opened a bank account in the Société générale." %} | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     <div class="card-body" id="profile_infos"> | ||||
|                         {% csrf_token %} | ||||
|                         {{ form|crispy }} | ||||
| @@ -104,7 +111,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
|         soge_field.change(fillFields); | ||||
|  | ||||
|         {% if object.profile.soge %} | ||||
|         {% if declare_soge_account %} | ||||
|             soge_field.attr('checked', true); | ||||
|             fillFields(); | ||||
|         {% endif %} | ||||
|   | ||||
| @@ -5,6 +5,7 @@ from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import transaction | ||||
| from django.db.models import Q | ||||
| from django.shortcuts import resolve_url, redirect | ||||
| from django.urls import reverse_lazy | ||||
| @@ -23,7 +24,7 @@ from permission.models import Role | ||||
| from permission.views import ProtectQuerysetMixin | ||||
| from treasury.models import SogeCredit | ||||
|  | ||||
| from .forms import SignUpForm, ValidationForm | ||||
| from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm | ||||
| from .tables import FutureUserTable | ||||
| from .tokens import email_validation_token | ||||
|  | ||||
| @@ -41,12 +42,14 @@ class UserCreateView(CreateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None) | ||||
|         context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None) | ||||
|         del context["profile_form"].fields["section"] | ||||
|         del context["profile_form"].fields["report_frequency"] | ||||
|         del context["profile_form"].fields["last_report"] | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         If the form is valid, then the user is created with is_active set to False | ||||
| @@ -70,6 +73,13 @@ class UserCreateView(CreateView): | ||||
|  | ||||
|         user.profile.send_email_validation_link() | ||||
|  | ||||
|         soge_form = DeclareSogeAccountOpenedForm(self.request.POST) | ||||
|         if "soge_account" in soge_form.data and soge_form.data["soge_account"]: | ||||
|             # If the user declares that a bank account got opened, prepare the soge credit to warn treasurers | ||||
|             soge_credit = SogeCredit(user=user) | ||||
|             soge_credit._force_save = True | ||||
|             soge_credit.save() | ||||
|  | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     def get_success_url(self): | ||||
| @@ -180,7 +190,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi | ||||
|                 | Q(username__iregex="^" + pattern) | ||||
|             ) | ||||
|  | ||||
|         return qs[:20] | ||||
|         return qs | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
| @@ -225,6 +235,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, | ||||
|         fee += 8000 | ||||
|         ctx["total_fee"] = "{:.02f}".format(fee / 100, ) | ||||
|  | ||||
|         ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists() | ||||
|  | ||||
|         return ctx | ||||
|  | ||||
|     def get_form(self, form_class=None): | ||||
| @@ -234,6 +246,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, | ||||
|         form.fields["first_name"].initial = user.first_name | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         user = self.get_object() | ||||
|  | ||||
| @@ -304,6 +317,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, | ||||
|         user.profile.save() | ||||
|         user.refresh_from_db() | ||||
|  | ||||
|         if not soge and SogeCredit.objects.filter(user=user).exists(): | ||||
|             # If the user declared that a bank account was opened but in the validation form the SoGé case was | ||||
|             # unchecked, delete the associated credit | ||||
|             soge_credit = SogeCredit.objects.get(user=user) | ||||
|             soge_credit._force_delete = True | ||||
|             soge_credit.delete() | ||||
|  | ||||
|         if credit_type is not None and credit_amount > 0: | ||||
|             # Credit the note | ||||
|             SpecialTransaction.objects.create( | ||||
| @@ -370,6 +390,8 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View): | ||||
|         user = User.objects.filter(profile__registration_valid=False)\ | ||||
|             .filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         # Delete associated soge credits before | ||||
|         SogeCredit.objects.filter(user=user).delete() | ||||
|  | ||||
|         user.delete() | ||||
|  | ||||
|   | ||||
 Submodule apps/scripts updated: e5b76b7c35...dbe7bf6591
									
								
							| @@ -16,10 +16,11 @@ class InvoiceViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/treasury/invoice/ | ||||
|     """ | ||||
|     queryset = Invoice.objects.order_by("id").all() | ||||
|     queryset = Invoice.objects.order_by('id') | ||||
|     serializer_class = InvoiceSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['bde', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ] | ||||
|     search_fields = ['$object', '$description', '$name', '$address', ] | ||||
|  | ||||
|  | ||||
| class ProductViewSet(ReadProtectedModelViewSet): | ||||
| @@ -28,10 +29,11 @@ class ProductViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/treasury/product/ | ||||
|     """ | ||||
|     queryset = Product.objects.order_by("invoice_id", "id").all() | ||||
|     queryset = Product.objects.order_by('invoice_id', 'id') | ||||
|     serializer_class = ProductSerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     search_fields = ['$designation', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ] | ||||
|     search_fields = ['$designation', '$invoice__object', ] | ||||
|  | ||||
|  | ||||
| class RemittanceTypeViewSet(ReadProtectedModelViewSet): | ||||
| @@ -40,8 +42,11 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer | ||||
|     then render it on /api/treasury/remittance_type/ | ||||
|     """ | ||||
|     queryset = RemittanceType.objects.order_by("id") | ||||
|     queryset = RemittanceType.objects.order_by('id') | ||||
|     serializer_class = RemittanceTypeSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['note', ] | ||||
|     search_fields = ['$note__special_type', ] | ||||
|  | ||||
|  | ||||
| class RemittanceViewSet(ReadProtectedModelViewSet): | ||||
| @@ -50,8 +55,11 @@ class RemittanceViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/treasury/remittance/ | ||||
|     """ | ||||
|     queryset = Remittance.objects.order_by("id") | ||||
|     queryset = Remittance.objects.order_by('id') | ||||
|     serializer_class = RemittanceSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ] | ||||
|     search_fields = ['$remittance_type__note__special_type', '$comment', ] | ||||
|  | ||||
|  | ||||
| class SogeCreditViewSet(ReadProtectedModelViewSet): | ||||
| @@ -60,5 +68,10 @@ class SogeCreditViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/treasury/soge_credit/ | ||||
|     """ | ||||
|     queryset = SogeCredit.objects.order_by("id") | ||||
|     queryset = SogeCredit.objects.order_by('id') | ||||
|     serializer_class = SogeCreditSerializer | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name', | ||||
|                         'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ] | ||||
|     search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name', | ||||
|                      '$user__note__alias__normalized_name', ] | ||||
|   | ||||
| @@ -28,6 +28,8 @@ class TreasuryConfig(AppConfig): | ||||
|                     source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), | ||||
|                     specialtransactionproxy=None, | ||||
|             ): | ||||
|                 SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None) | ||||
|                 proxy = SpecialTransactionProxy(transaction=transaction, remittance=None) | ||||
|                 proxy._force_save = True | ||||
|                 proxy.save() | ||||
|  | ||||
|         post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Submit | ||||
| from django import forms | ||||
| from django.db import transaction | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from note_kfet.inputs import AmountInput | ||||
|  | ||||
| @@ -149,6 +150,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm): | ||||
|         self.instance.transaction.bank = cleaned_data["bank"] | ||||
|         return cleaned_data | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, commit=True): | ||||
|         """ | ||||
|         Save the transaction and the remittance. | ||||
|   | ||||
| @@ -5,12 +5,12 @@ from datetime import date | ||||
|  | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.db import models, transaction | ||||
| from django.db.models import Q | ||||
| from django.template.loader import render_to_string | ||||
| from django.utils import timezone | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction | ||||
| from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser | ||||
|  | ||||
|  | ||||
| class Invoice(models.Model): | ||||
| @@ -76,6 +76,7 @@ class Invoice(models.Model): | ||||
|         verbose_name=_("tex source"), | ||||
|     ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         """ | ||||
|         When an invoice is generated, we store the tex source. | ||||
| @@ -228,6 +229,7 @@ class Remittance(models.Model): | ||||
|         """ | ||||
|         return sum(transaction.total for transaction in self.transactions.all()) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, force_insert=False, force_update=False, using=None, update_fields=None): | ||||
|         # Check if all transactions have the right type. | ||||
|         if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): | ||||
| @@ -255,6 +257,7 @@ class SpecialTransactionProxy(models.Model): | ||||
|         Remittance, | ||||
|         on_delete=models.PROTECT, | ||||
|         null=True, | ||||
|         related_name="transaction_proxies", | ||||
|         verbose_name=_("Remittance"), | ||||
|     ) | ||||
|  | ||||
| @@ -291,11 +294,12 @@ class SogeCredit(models.Model): | ||||
|  | ||||
|     @property | ||||
|     def valid(self): | ||||
|         return self.credit_transaction.valid | ||||
|         return self.credit_transaction and self.credit_transaction.valid | ||||
|  | ||||
|     @property | ||||
|     def amount(self): | ||||
|         return sum(transaction.total for transaction in self.transactions.all()) + 8000 | ||||
|         return self.credit_transaction.total if self.valid \ | ||||
|             else sum(transaction.total for transaction in self.transactions.all()) + 8000 | ||||
|  | ||||
|     def invalidate(self): | ||||
|         """ | ||||
| @@ -305,10 +309,10 @@ class SogeCredit(models.Model): | ||||
|         if self.valid: | ||||
|             self.credit_transaction.valid = False | ||||
|             self.credit_transaction.save() | ||||
|         for transaction in self.transactions.all(): | ||||
|             transaction.valid = False | ||||
|             transaction._force_save = True | ||||
|             transaction.save() | ||||
|         for tr in self.transactions.all(): | ||||
|             tr.valid = False | ||||
|             tr._force_save = True | ||||
|             tr.save() | ||||
|  | ||||
|     def validate(self, force=False): | ||||
|         if self.valid and not force: | ||||
| @@ -320,18 +324,25 @@ class SogeCredit(models.Model): | ||||
|         # Refresh credit amount | ||||
|         self.save() | ||||
|         self.credit_transaction.valid = True | ||||
|         self.credit_transaction._force_save = True | ||||
|         self.credit_transaction.save() | ||||
|         self.save() | ||||
|  | ||||
|         for transaction in self.transactions.all(): | ||||
|             transaction.valid = True | ||||
|             transaction._force_save = True | ||||
|             transaction.created_at = timezone.now() | ||||
|             transaction.save() | ||||
|         for tr in self.transactions.all(): | ||||
|             tr.valid = True | ||||
|             tr._force_save = True | ||||
|             tr.created_at = timezone.now() | ||||
|             tr.save() | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def save(self, *args, **kwargs): | ||||
|         # This is a pre-registered user that declared that a SoGé account was opened. | ||||
|         # No note exists yet. | ||||
|         if not NoteUser.objects.filter(user=self.user).exists(): | ||||
|             return super().save(*args, **kwargs) | ||||
|  | ||||
|         if not self.credit_transaction: | ||||
|             self.credit_transaction = SpecialTransaction.objects.create( | ||||
|             credit_transaction = SpecialTransaction( | ||||
|                 source=NoteSpecial.objects.get(special_type="Virement bancaire"), | ||||
|                 destination=self.user.note, | ||||
|                 quantity=1, | ||||
| @@ -342,6 +353,10 @@ class SogeCredit(models.Model): | ||||
|                 bank="Société générale", | ||||
|                 valid=False, | ||||
|             ) | ||||
|             credit_transaction._force_save = True | ||||
|             credit_transaction.save() | ||||
|             credit_transaction.refresh_from_db() | ||||
|             self.credit_transaction = credit_transaction | ||||
|         elif not self.valid: | ||||
|             self.credit_transaction.amount = self.amount | ||||
|             self.credit_transaction._force_save = True | ||||
| @@ -361,11 +376,11 @@ class SogeCredit(models.Model): | ||||
|                                     "Please ask her/him to credit the note before invalidating this credit.")) | ||||
|  | ||||
|         self.invalidate() | ||||
|         for transaction in self.transactions.all(): | ||||
|             transaction._force_save = True | ||||
|             transaction.valid = True | ||||
|             transaction.created_at = timezone.now() | ||||
|             transaction.save() | ||||
|         for tr in self.transactions.all(): | ||||
|             tr._force_save = True | ||||
|             tr.valid = True | ||||
|             tr.created_at = timezone.now() | ||||
|             tr.save() | ||||
|         self.credit_transaction.valid = False | ||||
|         self.credit_transaction.reason += " (invalide)" | ||||
|         self.credit_transaction.save() | ||||
|   | ||||
| @@ -10,9 +10,8 @@ def save_special_transaction(instance, created, **kwargs): | ||||
|     """ | ||||
|  | ||||
|     if not hasattr(instance, "_no_signal"): | ||||
|         if instance.is_credit(): | ||||
|             if created and RemittanceType.objects.filter(note=instance.source).exists(): | ||||
|                 SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save() | ||||
|         else: | ||||
|             if created and RemittanceType.objects.filter(note=instance.destination).exists(): | ||||
|                 SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save() | ||||
|         if created and RemittanceType.objects.filter( | ||||
|                 note=instance.source if instance.is_credit() else instance.destination).exists(): | ||||
|             proxy = SpecialTransactionProxy(transaction=instance, remittance=None) | ||||
|             proxy._force_save = True | ||||
|             proxy.save() | ||||
|   | ||||
| @@ -109,9 +109,6 @@ class SpecialTransactionTable(tables.Table): | ||||
|                                               'a': {'class': 'btn btn-primary btn-danger'} | ||||
|                                           }, ) | ||||
|  | ||||
|     def render_id(self, record): | ||||
|         return record.specialtransactionproxy.pk | ||||
|  | ||||
|     def render_amount(self, value): | ||||
|         return pretty_money(value) | ||||
|  | ||||
| @@ -147,4 +144,4 @@ class SogeCreditTable(tables.Table): | ||||
|  | ||||
|     class Meta: | ||||
|         model = SogeCredit | ||||
|         fields = ('user', 'amount', 'valid', ) | ||||
|         fields = ('user', 'user__last_name', 'user__first_name', 'amount', 'valid', ) | ||||
|   | ||||
| @@ -11,8 +11,14 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     </div> | ||||
|     <div class="card-body"> | ||||
|         <dl class="row"> | ||||
|             <dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd> | ||||
|             <dt class="col-xl-6 text-right">{% trans 'last name'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ object.user.last_name }}</dd> | ||||
|  | ||||
|             <dt class="col-xl-6 text-right">{% trans 'first name'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ object.user.first_name }}</dd> | ||||
|  | ||||
|             <dt class="col-xl-6 text-right">{% trans 'username'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user.username }}</a></dd> | ||||
|  | ||||
|             {% if "note.view_note_balance"|has_perm:object.user.note %} | ||||
|             <dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt> | ||||
|   | ||||
| @@ -60,7 +60,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             let pattern = searchbar_obj.val(); | ||||
|  | ||||
|             $("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + ( | ||||
|                 invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table"); | ||||
|                 invalid_only_obj.is(':checked') ? "" : "&valid=1") + " #credits_table"); | ||||
|  | ||||
|             $(".table-row").click(function () { | ||||
|                 window.document.location = $(this).data("href"); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from api.tests import TestAPI | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db.models import Q | ||||
| @@ -8,7 +9,10 @@ from django.test import TestCase | ||||
| from django.urls import reverse | ||||
| from member.models import Membership, Club | ||||
| from note.models import SpecialTransaction, NoteSpecial, Transaction | ||||
| from treasury.models import Invoice, Product, Remittance, RemittanceType, SogeCredit | ||||
|  | ||||
| from ..api.views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, \ | ||||
|     SogeCreditViewSet | ||||
| from ..models import Invoice, Product, Remittance, RemittanceType, SogeCredit | ||||
|  | ||||
|  | ||||
| class TestInvoices(TestCase): | ||||
| @@ -366,11 +370,8 @@ class TestSogeCredits(TestCase): | ||||
|         response = self.client.get(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,))) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|         try: | ||||
|             self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True)) | ||||
|             raise AssertionError("It is not possible to delete the soge credit until the note is not credited.") | ||||
|         except ValidationError: | ||||
|             pass | ||||
|         self.assertRaises(ValidationError, self.client.post, | ||||
|                           reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)), data=dict(delete=True)) | ||||
|  | ||||
|         SpecialTransaction.objects.create( | ||||
|             source=NoteSpecial.objects.get(special_type="Carte bancaire"), | ||||
| @@ -399,3 +400,82 @@ class TestSogeCredits(TestCase): | ||||
|         """ | ||||
|         response = self.client.get("/api/treasury/soge_credit/") | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|  | ||||
| class TestTreasuryAPI(TestAPI): | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|  | ||||
|         self.invoice = Invoice.objects.create( | ||||
|             id=1, | ||||
|             object="Object", | ||||
|             description="Description", | ||||
|             name="Me", | ||||
|             address="Earth", | ||||
|             acquitted=False, | ||||
|         ) | ||||
|         self.product = Product.objects.create( | ||||
|             invoice=self.invoice, | ||||
|             designation="Product", | ||||
|             quantity=3, | ||||
|             amount=3.14, | ||||
|         ) | ||||
|  | ||||
|         self.credit = SpecialTransaction.objects.create( | ||||
|             source=NoteSpecial.objects.get(special_type="Chèque"), | ||||
|             destination=self.user.note, | ||||
|             amount=4200, | ||||
|             reason="Credit", | ||||
|             last_name="TOTO", | ||||
|             first_name="Toto", | ||||
|             bank="Société générale", | ||||
|         ) | ||||
|  | ||||
|         self.remittance = Remittance.objects.create( | ||||
|             remittance_type=RemittanceType.objects.get(), | ||||
|             comment="Test remittance", | ||||
|             closed=False, | ||||
|         ) | ||||
|         self.credit.specialtransactionproxy.remittance = self.remittance | ||||
|         self.credit.specialtransactionproxy.save() | ||||
|  | ||||
|         self.kfet = Club.objects.get(name="Kfet") | ||||
|         self.bde = self.kfet.parent_club | ||||
|  | ||||
|         self.kfet_membership = Membership( | ||||
|             user=self.user, | ||||
|             club=self.kfet, | ||||
|         ) | ||||
|         self.kfet_membership._force_renew_parent = True | ||||
|         self.kfet_membership._soge = True | ||||
|         self.kfet_membership.save() | ||||
|  | ||||
|     def test_invoice_api(self): | ||||
|         """ | ||||
|         Load Invoice API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(InvoiceViewSet, "/api/treasury/invoice/") | ||||
|  | ||||
|     def test_product_api(self): | ||||
|         """ | ||||
|         Load Product API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(ProductViewSet, "/api/treasury/product/") | ||||
|  | ||||
|     def test_remittance_api(self): | ||||
|         """ | ||||
|         Load Remittance API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(RemittanceViewSet, "/api/treasury/remittance/") | ||||
|  | ||||
|     def test_remittance_type_api(self): | ||||
|         """ | ||||
|         Load RemittanceType API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(RemittanceTypeViewSet, "/api/treasury/remittance_type/") | ||||
|  | ||||
|     def test_sogecredit_api(self): | ||||
|         """ | ||||
|         Load SogeCredit API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(SogeCreditViewSet, "/api/treasury/soge_credit/") | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from tempfile import mkdtemp | ||||
| from crispy_forms.helper import FormHelper | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.core.exceptions import ValidationError, PermissionDenied | ||||
| from django.db import transaction | ||||
| from django.db.models import Q | ||||
| from django.forms import Form | ||||
| from django.http import HttpResponse | ||||
| @@ -65,6 +66,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         del form.fields["locked"] | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         ret = super().form_valid(form) | ||||
|  | ||||
| @@ -144,6 +146,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|         del form.fields["id"] | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         ret = super().form_valid(form) | ||||
|  | ||||
| @@ -428,7 +431,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi | ||||
|         if "valid" not in self.request.GET or not self.request.GET["valid"]: | ||||
|             qs = qs.filter(credit_transaction__valid=False) | ||||
|  | ||||
|         return qs[:20] | ||||
|         return qs | ||||
|  | ||||
|  | ||||
| class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView): | ||||
| @@ -439,6 +442,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie | ||||
|     form_class = Form | ||||
|     extra_context = {"title": _("Manage credits from the Société générale")} | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         if "validate" in form.data: | ||||
|             self.get_object().validate(True) | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework.filters import SearchFilter | ||||
| from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
|  | ||||
| from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \ | ||||
| @@ -15,11 +16,14 @@ class WEIClubViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/club/ | ||||
|     """ | ||||
|     queryset = WEIClub.objects.all() | ||||
|     queryset = WEIClub.objects.order_by('id') | ||||
|     serializer_class = WEIClubSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$name', ] | ||||
|     filterset_fields = ['name', 'year', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name', | ||||
|                         'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships', | ||||
|                         'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start', | ||||
|                         'membership_end', ] | ||||
|     search_fields = ['$name', '$email', '$note__alias__name', '$note__alias__normalized_name', ] | ||||
|  | ||||
|  | ||||
| class BusViewSet(ReadProtectedModelViewSet): | ||||
| @@ -28,11 +32,11 @@ class BusViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/bus/ | ||||
|     """ | ||||
|     queryset = Bus.objects | ||||
|     queryset = Bus.objects.order_by('id') | ||||
|     serializer_class = BusSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$name', ] | ||||
|     filterset_fields = ['name', 'wei', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'wei', 'description', ] | ||||
|     search_fields = ['$name', '$wei__name', '$description', ] | ||||
|  | ||||
|  | ||||
| class BusTeamViewSet(ReadProtectedModelViewSet): | ||||
| @@ -41,11 +45,11 @@ class BusTeamViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/team/ | ||||
|     """ | ||||
|     queryset = BusTeam.objects | ||||
|     queryset = BusTeam.objects.order_by('id') | ||||
|     serializer_class = BusTeamSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$name', ] | ||||
|     filterset_fields = ['name', 'bus', 'bus__wei', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ] | ||||
|     search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ] | ||||
|  | ||||
|  | ||||
| class WEIRoleViewSet(ReadProtectedModelViewSet): | ||||
| @@ -54,9 +58,10 @@ class WEIRoleViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/role/ | ||||
|     """ | ||||
|     queryset = WEIRole.objects | ||||
|     queryset = WEIRole.objects.order_by('id') | ||||
|     serializer_class = WEIRoleSerializer | ||||
|     filter_backends = [SearchFilter] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['name', 'permissions', 'memberships', ] | ||||
|     search_fields = ['$name', ] | ||||
|  | ||||
|  | ||||
| @@ -66,11 +71,17 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/registration/ | ||||
|     """ | ||||
|     queryset = WEIRegistration.objects | ||||
|     queryset = WEIRegistration.objects.order_by('id') | ||||
|     serializer_class = WEIRegistrationSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$user__username', ] | ||||
|     filterset_fields = ['user', 'wei', ] | ||||
|     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||
|     filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email', | ||||
|                         'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name', | ||||
|                         'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender', | ||||
|                         'clothing_cut', 'clothing_size', 'first_year', 'emergency_contact_name', | ||||
|                         'emergency_contact_phone', ] | ||||
|     search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email', | ||||
|                      '$user__note__alias__name', '$user__note__alias__normalized_name', '$wei__name', | ||||
|                      '$wei__email', '$health_issues', '$emergency_contact_name', '$emergency_contact_phone', ] | ||||
|  | ||||
|  | ||||
| class WEIMembershipViewSet(ReadProtectedModelViewSet): | ||||
| @@ -79,8 +90,16 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet): | ||||
|     The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/wei/membership/ | ||||
|     """ | ||||
|     queryset = WEIMembership.objects | ||||
|     queryset = WEIMembership.objects.order_by('id') | ||||
|     serializer_class = WEIMembershipSerializer | ||||
|     filter_backends = [SearchFilter, DjangoFilterBackend] | ||||
|     search_fields = ['$user__username', '$bus__name', '$team__name', ] | ||||
|     filterset_fields = ['user', 'club', 'bus', 'team', ] | ||||
|     filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||
|     filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', | ||||
|                         'club__note__alias__normalized_name', 'user__username', 'user__last_name', | ||||
|                         'user__first_name', 'user__email', 'user__note__alias__name', | ||||
|                         'user__note__alias__normalized_name', 'date_start', 'date_end', 'fee', 'roles', 'bus', | ||||
|                         'bus__name', 'team', 'team__name', 'registration', ] | ||||
|     ordering_fields = ['id', 'date_start', 'date_end', ] | ||||
|     search_fields = ['$club__name', '$club__email', '$club__note__alias__name', | ||||
|                      '$club__note__alias__normalized_name', '$user__username', '$user__last_name', | ||||
|                      '$user__first_name', '$user__email', '$user__note__alias__name', | ||||
|                      '$user__note__alias__normalized_name', '$roles__name', '$bus__name', '$team__name', ] | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| from random import choice | ||||
|  | ||||
| from django import forms | ||||
| from django.db import transaction | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation | ||||
| @@ -88,6 +89,7 @@ class WEISurvey2020(WEISurvey): | ||||
|         """ | ||||
|         form.set_registration(self.registration) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         word = form.cleaned_data["word"] | ||||
|         self.information.step += 1 | ||||
|   | ||||
| @@ -1,59 +0,0 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import date | ||||
|  | ||||
| from django.core.management import BaseCommand | ||||
| from django.db.models import Q | ||||
| from member.models import Membership, Club | ||||
| from wei.models import WEIClub | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Get mailing list registrations from the last wei. " \ | ||||
|            "Usage: manage.py extract_ml_registrations -t {events,art,sport}. " \ | ||||
|            "You can write this into a file with a pipe, then paste the document into your mail manager." | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument('--type', '-t', choices=["members", "clubs", "events", "art", "sport"], default="members", | ||||
|                             help='Select the type of the mailing list (default members)') | ||||
|         parser.add_argument('--year', '-y', type=int, default=None, | ||||
|                             help='Select the year of the concerned WEI. Default: last year') | ||||
|  | ||||
|     def handle(self, *args, **options): | ||||
|         ########################################################### | ||||
|         #                         WARNING                         # | ||||
|         ########################################################### | ||||
|         # | ||||
|         # This code is obsolete. | ||||
|         # TODO: Improve the mailing list extraction system, and link it automatically with Mailman. | ||||
|  | ||||
|         if options["type"] == "members": | ||||
|             for membership in Membership.objects.filter( | ||||
|                 club__name="BDE", | ||||
|                 date_start__lte=date.today(), | ||||
|                 date_end__gte=date.today(), | ||||
|             ).all(): | ||||
|                 self.stdout.write(membership.user.email) | ||||
|             return | ||||
|  | ||||
|         if options["type"] == "clubs": | ||||
|             for club in Club.objects.all(): | ||||
|                 self.stdout.write(club.email) | ||||
|             return | ||||
|  | ||||
|         if options["year"] is None: | ||||
|             wei = WEIClub.objects.order_by('-year').first() | ||||
|         else: | ||||
|             wei = WEIClub.objects.filter(year=options["year"]) | ||||
|             if wei.exists(): | ||||
|                 wei = wei.get() | ||||
|             else: | ||||
|                 wei = WEIClub.objects.order_by('-year').first() | ||||
|                 self.stderr.write(self.style.WARNING("Warning: there was no WEI in year " + str(options["year"]) + ". " | ||||
|                                                      + "Assuming the last WEI (year " + str(wei.year) + ")")) | ||||
|         q = Q(ml_events_registration=True) if options["type"] == "events" else Q(ml_art_registration=True)\ | ||||
|             if options["type"] == "art" else Q(ml_sport_registration=True) | ||||
|         registrations = wei.users.filter(q) | ||||
|         for registration in registrations.all(): | ||||
|             self.stdout.write(registration.user.email) | ||||
| @@ -238,7 +238,7 @@ class WEIRegistration(models.Model): | ||||
|     information_json = models.TextField( | ||||
|         default="{}", | ||||
|         verbose_name=_("registration information"), | ||||
|         help_text=_("Information about the registration (buses for old members, survey fot the new members), " | ||||
|         help_text=_("Information about the registration (buses for old members, survey for the new members), " | ||||
|                     "encoded in JSON"), | ||||
|     ) | ||||
|  | ||||
|   | ||||
| @@ -61,10 +61,10 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                     <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     {% if "note.change_alias"|has_perm:club.note.alias_set.first %} | ||||
|                     {% if "note.change_alias"|has_perm:club.note.alias.first %} | ||||
|                     <dt class="col-xl-4"><a | ||||
|                             href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> | ||||
|                     <dd class="col-xl-8 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd> | ||||
|                     <dd class="col-xl-8 text-truncate">{{ club.note.alias.all|join:", " }}</dd> | ||||
|                     {% endif %} | ||||
|  | ||||
|                     <dt class="col-xl-4">{% trans 'email'|capfirst %}</dt> | ||||
|   | ||||
| @@ -4,16 +4,19 @@ | ||||
| import subprocess | ||||
| from datetime import timedelta, date | ||||
|  | ||||
| from api.tests import TestAPI | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.db.models import Q | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
| from django.utils import timezone | ||||
| from member.models import Membership | ||||
| from member.models import Membership, Club | ||||
| from note.models import NoteClub, SpecialTransaction | ||||
| from treasury.models import SogeCredit | ||||
|  | ||||
| from ..api.views import BusViewSet, BusTeamViewSet, WEIClubViewSet, WEIMembershipViewSet, WEIRegistrationViewSet, \ | ||||
|     WEIRoleViewSet | ||||
| from ..forms import CurrentSurvey, WEISurveyAlgorithm, WEISurvey | ||||
| from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership | ||||
|  | ||||
| @@ -524,7 +527,7 @@ class TestWEIRegistration(TestCase): | ||||
|         sess["permission_mask"] = 0 | ||||
|         sess.save() | ||||
|         response = self.client.get(reverse("wei:wei_update_registration", kwargs=dict(pk=self.registration.pk))) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertEqual(response.status_code, 403) | ||||
|         sess["permission_mask"] = 42 | ||||
|         sess.save() | ||||
|  | ||||
| @@ -807,3 +810,97 @@ class TestWEISurveyAlgorithm(TestCase): | ||||
|  | ||||
|     def test_survey_algorithm(self): | ||||
|         CurrentSurvey.get_algorithm_class()().run_algorithm() | ||||
|  | ||||
|  | ||||
| class TestWeiAPI(TestAPI): | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|  | ||||
|         self.year = timezone.now().year | ||||
|         self.wei = WEIClub.objects.create( | ||||
|             name="Test WEI", | ||||
|             email="gc.wei@example.com", | ||||
|             parent_club_id=2, | ||||
|             membership_fee_paid=12500, | ||||
|             membership_fee_unpaid=5500, | ||||
|             membership_start=date(self.year, 1, 1), | ||||
|             membership_end=date(self.year, 12, 31), | ||||
|             membership_duration=396, | ||||
|             year=self.year, | ||||
|             date_start=date.today() + timedelta(days=2), | ||||
|             date_end=date(self.year, 12, 31), | ||||
|         ) | ||||
|         NoteClub.objects.create(club=self.wei) | ||||
|         self.bus = Bus.objects.create( | ||||
|             name="Test Bus", | ||||
|             wei=self.wei, | ||||
|             description="Test Bus", | ||||
|         ) | ||||
|         self.team = BusTeam.objects.create( | ||||
|             name="Test Team", | ||||
|             bus=self.bus, | ||||
|             color=0xFFFFFF, | ||||
|             description="Test Team", | ||||
|         ) | ||||
|         self.registration = WEIRegistration.objects.create( | ||||
|             user_id=self.user.id, | ||||
|             wei_id=self.wei.id, | ||||
|             soge_credit=True, | ||||
|             caution_check=True, | ||||
|             birth_date=date(2000, 1, 1), | ||||
|             gender="nonbinary", | ||||
|             clothing_cut="male", | ||||
|             clothing_size="XL", | ||||
|             health_issues="I am a bot", | ||||
|             emergency_contact_name="Pikachu", | ||||
|             emergency_contact_phone="+33123456789", | ||||
|             first_year=False, | ||||
|         ) | ||||
|         Membership.objects.create(user=self.user, club=Club.objects.get(name="BDE")) | ||||
|         Membership.objects.create(user=self.user, club=Club.objects.get(name="Kfet")) | ||||
|         self.membership = WEIMembership.objects.create( | ||||
|             user=self.user, | ||||
|             club=self.wei, | ||||
|             fee=125, | ||||
|             bus=self.bus, | ||||
|             team=self.team, | ||||
|             registration=self.registration, | ||||
|         ) | ||||
|         self.membership.roles.add(WEIRole.objects.last()) | ||||
|         self.membership.save() | ||||
|  | ||||
|     def test_weiclub_api(self): | ||||
|         """ | ||||
|         Load WEI API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(WEIClubViewSet, "/api/wei/club/") | ||||
|  | ||||
|     def test_wei_bus_api(self): | ||||
|         """ | ||||
|         Load Bus API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(BusViewSet, "/api/wei/bus/") | ||||
|  | ||||
|     def test_wei_team_api(self): | ||||
|         """ | ||||
|         Load BusTeam API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(BusTeamViewSet, "/api/wei/team/") | ||||
|  | ||||
|     def test_weirole_api(self): | ||||
|         """ | ||||
|         Load WEIRole API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(WEIRoleViewSet, "/api/wei/role/") | ||||
|  | ||||
|     def test_weiregistration_api(self): | ||||
|         """ | ||||
|         Load WEIRegistration API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(WEIRegistrationViewSet, "/api/wei/registration/") | ||||
|  | ||||
|     def test_weimembership_api(self): | ||||
|         """ | ||||
|         Load WEIMembership API page and test all filters and permissions | ||||
|         """ | ||||
|         self.check_viewset(WEIMembershipViewSet, "/api/wei/membership/") | ||||
|   | ||||
| @@ -10,6 +10,7 @@ from tempfile import mkdtemp | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.db import transaction | ||||
| from django.db.models import Q, Count | ||||
| from django.db.models.functions.text import Lower | ||||
| from django.forms import HiddenInput | ||||
| @@ -84,6 +85,7 @@ class WEICreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|             date_end=date.today(), | ||||
|         ) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         form.instance.requires_membership = True | ||||
|         form.instance.parent_club = Club.objects.get(name="Kfet") | ||||
| @@ -517,6 +519,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|         del form.fields["information_json"] | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) | ||||
|         form.instance.first_year = True | ||||
| @@ -597,6 +600,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|  | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) | ||||
|         form.instance.first_year = False | ||||
| @@ -688,6 +692,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update | ||||
|             del form.fields["information_json"] | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         # If the membership is already validated, then we update the bus and the team (and the roles) | ||||
|         if form.instance.is_validated: | ||||
| @@ -866,6 +871,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|                 ).all() | ||||
|         return form | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Create membership, check that all is good, make transactions | ||||
| @@ -1016,6 +1022,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): | ||||
|         context["club"] = self.object.wei | ||||
|         return context | ||||
|  | ||||
|     @transaction.atomic | ||||
|     def form_valid(self, form): | ||||
|         """ | ||||
|         Update the survey with the data of the form. | ||||
|   | ||||
| @@ -14,6 +14,7 @@ fi | ||||
| # Set up Django project | ||||
| python3 manage.py collectstatic --noinput | ||||
| python3 manage.py compilemessages | ||||
| python3 manage.py compilejsmessages | ||||
| python3 manage.py migrate | ||||
|  | ||||
| if [ "$1" ]; then | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										133
									
								
								locale/de/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								locale/de/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| # SOME DESCRIPTIVE TITLE. | ||||
| # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | ||||
| # This file is distributed under the same license as the PACKAGE package. | ||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | ||||
| # | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-11-15 23:21+0100\n" | ||||
| "PO-Revision-Date: 2020-11-16 20:21+0000\n" | ||||
| "Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n" | ||||
| "Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20-js/de/>" | ||||
| "\n" | ||||
| "Language: de\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=n != 1;\n" | ||||
| "X-Generator: Weblate 4.3.2\n" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:17 | ||||
| msgid "Alias successfully added" | ||||
| msgstr "Alias erfolgreich hinzugefügt" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:33 | ||||
| msgid "Alias successfully deleted" | ||||
| msgstr "Alias erfolgreich gelöscht" | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:225 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is very negative." | ||||
| msgstr "" | ||||
| "Warnung, die Transaktion aus der Note %s gelingt, aber die Emitternote %s " | ||||
| "ist sehr negativ." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:228 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is negative." | ||||
| msgstr "" | ||||
| "Warnung, die Transaktion aus der Note %s gelingt, aber die Emitternote %s " | ||||
| "ist negativ." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:232 | ||||
| #: apps/note/static/note/js/transfer.js:298 | ||||
| #: apps/note/static/note/js/transfer.js:401 | ||||
| #, javascript-format | ||||
| msgid "Warning, the emitter note %s is no more a BDE member." | ||||
| msgstr "Warnung, der Emittent Hinweis %s ist kein BDE-Mitglied mehr." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:253 | ||||
| msgid "The transaction couldn't be validated because of insufficient balance." | ||||
| msgstr "" | ||||
| "Die Transaktion konnte aufgrund eines unzureichenden Saldos nicht validiert " | ||||
| "werden." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:238 | ||||
| msgid "This field is required and must contain a decimal positive number." | ||||
| msgstr "" | ||||
| "Dieses Feld ist erforderlich und muss eine positive Dezimalzahl enthalten." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:245 | ||||
| msgid "The amount must stay under 21,474,836.47 €." | ||||
| msgstr "Der Betrag muss unter 21.474.836,47 € bleiben." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:251 | ||||
| msgid "This field is required." | ||||
| msgstr "Dies ist ein Pflichtfeld." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:277 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning: the transaction of %s from %s to %s was not made because it is the " | ||||
| "same source and destination note." | ||||
| msgstr "" | ||||
| "Warnung: Die Transaktion von %s von %s nach %s wurde nicht durchgeführt, da " | ||||
| "es sich um die gleiche Quell- und Zielnotiz handelt." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:301 | ||||
| #, javascript-format | ||||
| msgid "Warning, the destination note %s is no more a BDE member." | ||||
| msgstr "Warnung, der Bestimmungsvermerk %s ist kein BDE-Mitglied mehr." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:307 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is very negative." | ||||
| msgstr "" | ||||
| "Warnung, die Transaktion von %s von der Note %s zur Note %s gelingt, aber " | ||||
| "die Emitternote %s ist sehr negativ." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:312 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is negative." | ||||
| msgstr "" | ||||
| "Warnung, die Transaktion von %s von der Note %s zur Note %s gelingt, aber " | ||||
| "die Emitternote %s ist negativ." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:318 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s succeed!" | ||||
| msgstr "Übertragung von %s von %s auf %s gelingt!" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:325 | ||||
| #: apps/note/static/note/js/transfer.js:346 | ||||
| #: apps/note/static/note/js/transfer.js:353 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s failed: %s" | ||||
| msgstr "Übertragung von %s von %s auf %s fehlgeschlagen: %s" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:347 | ||||
| msgid "insufficient funds" | ||||
| msgstr "unzureichende Geldmittel" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:400 | ||||
| msgid "Credit/debit succeed!" | ||||
| msgstr "Kredit/Debit erfolgreich!" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:407 | ||||
| #, javascript-format | ||||
| msgid "Credit/debit failed: %s" | ||||
| msgstr "Kredit/Debit fehlgeschlagen: %s" | ||||
|  | ||||
| #: note_kfet/static/js/base.js:366 | ||||
| msgid "An error occured while (in)validating this transaction:" | ||||
| msgstr "Bei der (Un-)Validierung dieser Transaktion ist ein Fehler aufgetreten:" | ||||
							
								
								
									
										3222
									
								
								locale/es/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3222
									
								
								locale/es/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										129
									
								
								locale/es/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								locale/es/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| # SOME DESCRIPTIVE TITLE. | ||||
| # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | ||||
| # This file is distributed under the same license as the PACKAGE package. | ||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | ||||
| # | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: \n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-11-15 23:21+0100\n" | ||||
| "PO-Revision-Date: 2020-11-21 12:23+0100\n" | ||||
| "Language: es\n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n != 1);\n" | ||||
| "Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n" | ||||
| "Language-Team: \n" | ||||
| "X-Generator: Poedit 2.3\n" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:17 | ||||
| msgid "Alias successfully added" | ||||
| msgstr "Alias añadido con éxito" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:33 | ||||
| msgid "Alias successfully deleted" | ||||
| msgstr "Alias suprimido con éxito" | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:225 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is very negative." | ||||
| msgstr "" | ||||
| "Cuidado, la transacción de %s fue un éxito, pero la note %s está muy " | ||||
| "negativa." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:228 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is negative." | ||||
| msgstr "" | ||||
| "Cuidado, la transacción de %s fue un éxito, pero la note %s está negativa." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:232 | ||||
| #: apps/note/static/note/js/transfer.js:298 | ||||
| #: apps/note/static/note/js/transfer.js:401 | ||||
| #, javascript-format | ||||
| msgid "Warning, the emitter note %s is no more a BDE member." | ||||
| msgstr "Cuidado, la note remitente %s no está más miembro del BDE." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:253 | ||||
| msgid "The transaction couldn't be validated because of insufficient balance." | ||||
| msgstr "" | ||||
| "La transacción no pudo ser validada por culpa de saldo demasiado bajo." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:238 | ||||
| msgid "This field is required and must contain a decimal positive number." | ||||
| msgstr "Este campo obligatorio requiere un número decimal positivo." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:245 | ||||
| msgid "The amount must stay under 21,474,836.47 €." | ||||
| msgstr "El monto no puede superar los 21 474 836,47 €." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:251 | ||||
| msgid "This field is required." | ||||
| msgstr "Este campo es obligatorio." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:277 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning: the transaction of %s from %s to %s was not made because it is the " | ||||
| "same source and destination note." | ||||
| msgstr "" | ||||
| "Cuidado : la transacción de %s de %s a %s no fue echa porque la fuente y el " | ||||
| "destino son iguales." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:301 | ||||
| #, javascript-format | ||||
| msgid "Warning, the destination note %s is no more a BDE member." | ||||
| msgstr "Cuidado, la note destino %s no está más miembro del BDE." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:307 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is very negative." | ||||
| msgstr "" | ||||
| "Cuidado, la transacción de %s de la note %s a la note %s fue un éxito, pero " | ||||
| "la note fuente %s está muy negativa." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:312 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is negative." | ||||
| msgstr "" | ||||
| "Cuidado, la transacción de %s de la note %s a la note %s fue un éxito, pero " | ||||
| "la note fuente %s está negativa." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:318 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s succeed!" | ||||
| msgstr "¡ La transacción de %s de %s a %s fue un éxito !" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:325 | ||||
| #: apps/note/static/note/js/transfer.js:346 | ||||
| #: apps/note/static/note/js/transfer.js:353 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s failed: %s" | ||||
| msgstr "La transacción de %s de %s a %s fue un fracaso : %s" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:347 | ||||
| msgid "insufficient funds" | ||||
| msgstr "fundos insuficientes" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:400 | ||||
| msgid "Credit/debit succeed!" | ||||
| msgstr "¡ Crédito/débito tubo éxito !" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:407 | ||||
| #, javascript-format | ||||
| msgid "Credit/debit failed: %s" | ||||
| msgstr "Crédito/débito falló : %s" | ||||
|  | ||||
| #: note_kfet/static/js/base.js:366 | ||||
| msgid "An error occured while (in)validating this transaction:" | ||||
| msgstr "Un error ocurrió durante la (in)validación de esta transacción :" | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										134
									
								
								locale/fr/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								locale/fr/LC_MESSAGES/djangojs.po
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| # SOME DESCRIPTIVE TITLE. | ||||
| # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | ||||
| # This file is distributed under the same license as the PACKAGE package. | ||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | ||||
| # | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-11-15 23:21+0100\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| "Language: \n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n > 1);\n" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:17 | ||||
| msgid "Alias successfully added" | ||||
| msgstr "Alias ajouté avec succès" | ||||
|  | ||||
| #: apps/member/static/member/js/alias.js:33 | ||||
| msgid "Alias successfully deleted" | ||||
| msgstr "Alias supprimé avec succès" | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:225 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is very negative." | ||||
| msgstr "" | ||||
| "Attention, La transaction depuis la note %s a été réalisée avec succès, mais " | ||||
| "la note émettrice %s est en négatif sévère." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:228 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction from the note %s succeed, but the emitter note %s " | ||||
| "is negative." | ||||
| msgstr "" | ||||
| "Attention, La transaction depuis la note %s a été réalisée avec succès, mais " | ||||
| "la note émettrice %s est en négatif." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:232 | ||||
| #: apps/note/static/note/js/transfer.js:298 | ||||
| #: apps/note/static/note/js/transfer.js:401 | ||||
| #, javascript-format | ||||
| msgid "Warning, the emitter note %s is no more a BDE member." | ||||
| msgstr "Attention, la note émettrice %s n'est plus adhérente." | ||||
|  | ||||
| #: apps/note/static/note/js/consos.js:253 | ||||
| msgid "The transaction couldn't be validated because of insufficient balance." | ||||
| msgstr "" | ||||
| "La transaction n'a pas pu être validée pour cause de solde insuffisant." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:238 | ||||
| msgid "This field is required and must contain a decimal positive number." | ||||
| msgstr "" | ||||
| "Ce champ est requis et doit comporter un nombre décimal strictement positif." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:245 | ||||
| msgid "The amount must stay under 21,474,836.47 €." | ||||
| msgstr "Le montant ne doit pas excéder 21 474 836.47 €." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:251 | ||||
| msgid "This field is required." | ||||
| msgstr "Ce champ est requis." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:277 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning: the transaction of %s from %s to %s was not made because it is the " | ||||
| "same source and destination note." | ||||
| msgstr "" | ||||
| "Attention : la transaction de %s de la note %s vers la note %s n'a pas été " | ||||
| "faite car il s'agit de la même note au départ et à l'arrivée." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:301 | ||||
| #, javascript-format | ||||
| msgid "Warning, the destination note %s is no more a BDE member." | ||||
| msgstr "Attention, la note de destination %s n'est plus adhérente." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:307 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is very negative." | ||||
| msgstr "" | ||||
| "Attention, La transaction de %s depuis la note %s vers la note %s a été " | ||||
| "réalisée avec succès, mais la note émettrice %s est en négatif sévère." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:312 | ||||
| #, javascript-format | ||||
| msgid "" | ||||
| "Warning, the transaction of %s from the note %s to the note %s succeed, but " | ||||
| "the emitter note %s is negative." | ||||
| msgstr "" | ||||
| "Attention, La transaction de %s depuis la note %s vers la note %s a été " | ||||
| "réalisée avec succès, mais la note émettrice %s est en négatif." | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:318 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s succeed!" | ||||
| msgstr "" | ||||
| "Le transfert de %s de la note %s vers la note %s a été fait avec succès !" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:325 | ||||
| #: apps/note/static/note/js/transfer.js:346 | ||||
| #: apps/note/static/note/js/transfer.js:353 | ||||
| #, javascript-format | ||||
| msgid "Transfer of %s from %s to %s failed: %s" | ||||
| msgstr "Le transfert de %s de la note %s vers la note %s a échoué : %s" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:347 | ||||
| msgid "insufficient funds" | ||||
| msgstr "solde insuffisant" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:400 | ||||
| msgid "Credit/debit succeed!" | ||||
| msgstr "Le crédit/retrait a bien été effectué !" | ||||
|  | ||||
| #: apps/note/static/note/js/transfer.js:407 | ||||
| #, javascript-format | ||||
| msgid "Credit/debit failed: %s" | ||||
| msgstr "Le crédit/retrait a échoué : %s" | ||||
|  | ||||
| #: note_kfet/static/js/base.js:366 | ||||
| msgid "An error occured while (in)validating this transaction:" | ||||
| msgstr "" | ||||
| "Une erreur est survenue lors de la validation/dévalidation de cette " | ||||
| "transaction :" | ||||
| @@ -20,3 +20,5 @@ | ||||
|  55  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py send_reports | ||||
| # Mettre à jour les boutons mis en avant | ||||
|  00  9     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py refresh_highlighted_buttons | ||||
| # Vider les tokens Oauth2 | ||||
|  00  6     *   *   *     root   cd /var/www/note_kfet && env/bin/python manage.py cleartokens | ||||
|   | ||||
| @@ -26,6 +26,14 @@ admin_site = StrongAdminSite() | ||||
| admin_site.register(Site, SiteAdmin) | ||||
|  | ||||
| # Add external apps model | ||||
| if "oauth2_provider" in settings.INSTALLED_APPS: | ||||
|     from oauth2_provider.admin import Application, ApplicationAdmin, Grant, \ | ||||
|         GrantAdmin, AccessToken, AccessTokenAdmin, RefreshToken, RefreshTokenAdmin | ||||
|     admin_site.register(Application, ApplicationAdmin) | ||||
|     admin_site.register(Grant, GrantAdmin) | ||||
|     admin_site.register(AccessToken, AccessTokenAdmin) | ||||
|     admin_site.register(RefreshToken, RefreshTokenAdmin) | ||||
|  | ||||
| if "django_htcpcp_tea" in settings.INSTALLED_APPS: | ||||
|     from django_htcpcp_tea.admin import * | ||||
|     from django_htcpcp_tea.models import * | ||||
| @@ -44,9 +52,3 @@ if "rest_framework" in settings.INSTALLED_APPS: | ||||
|     from rest_framework.authtoken.admin import * | ||||
|     from rest_framework.authtoken.models import * | ||||
|     admin_site.register(Token, TokenAdmin) | ||||
|  | ||||
| if "cas_server" in settings.INSTALLED_APPS: | ||||
|     from cas_server.admin import * | ||||
|     from cas_server.models import * | ||||
|     admin_site.register(ServicePattern, ServicePatternAdmin) | ||||
|     admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin) | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| [ | ||||
|     { | ||||
|         "model": "cas_server.servicepattern", | ||||
|         "pk": 1, | ||||
|         "fields": { | ||||
|             "pos": 1, | ||||
|             "pattern": ".*", | ||||
|             "name": "REPLACEME" | ||||
|         } | ||||
|     } | ||||
| ] | ||||
| @@ -3,7 +3,7 @@ | ||||
|         "model": "sites.site", | ||||
|         "pk": 1, | ||||
|         "fields": { | ||||
|             "domain": "localhost", | ||||
|             "domain": "note.crans.org", | ||||
|             "name": "La Note Kfet \ud83c\udf7b" | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -2,12 +2,12 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth import login | ||||
| from django.contrib.auth.models import AnonymousUser, User | ||||
| from django.contrib.sessions.backends.db import SessionStore | ||||
|  | ||||
| from threading import local | ||||
|  | ||||
| from django.contrib.sessions.backends.db import SessionStore | ||||
|  | ||||
| USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') | ||||
| SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') | ||||
| IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') | ||||
| @@ -50,6 +50,20 @@ class SessionMiddleware(object): | ||||
|  | ||||
|     def __call__(self, request): | ||||
|         user = request.user | ||||
|  | ||||
|         # If we authenticate through a token to connect to the API, then we query the good user | ||||
|         if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"): | ||||
|             token = request.META.get('HTTP_AUTHORIZATION') | ||||
|             if token.startswith("Token "): | ||||
|                 token = token[6:] | ||||
|                 from rest_framework.authtoken.models import Token | ||||
|                 if Token.objects.filter(key=token).exists(): | ||||
|                     token_obj = Token.objects.get(key=token) | ||||
|                     user = token_obj.user | ||||
|                     session = request.session | ||||
|                     session["permission_mask"] = 42 | ||||
|                     session.save() | ||||
|  | ||||
|         if 'HTTP_X_REAL_IP' in request.META: | ||||
|             ip = request.META.get('HTTP_X_REAL_IP') | ||||
|         elif 'HTTP_X_FORWARDED_FOR' in request.META: | ||||
| @@ -64,6 +78,41 @@ class SessionMiddleware(object): | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class LoginByIPMiddleware(object): | ||||
|     """ | ||||
|     Allow some users to be authenticated based on their IP address. | ||||
|     For example, the "note" account should not be used elsewhere than the Kfet computer, | ||||
|     and should not have any password. | ||||
|     The password that is stored in database should be on the form "ipbased$my.public.ip.address". | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, get_response): | ||||
|         self.get_response = get_response | ||||
|  | ||||
|     def __call__(self, request): | ||||
|         """ | ||||
|         If the user is not authenticated, get the used IP address | ||||
|         and check if an user is authorized to be automatically logged with this address. | ||||
|         If it is the case, the logging is performed with the full rights. | ||||
|         """ | ||||
|         if not request.user.is_authenticated: | ||||
|             if 'HTTP_X_REAL_IP' in request.META: | ||||
|                 ip = request.META.get('HTTP_X_REAL_IP') | ||||
|             elif 'HTTP_X_FORWARDED_FOR' in request.META: | ||||
|                 ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0] | ||||
|             else: | ||||
|                 ip = request.META.get('REMOTE_ADDR') | ||||
|  | ||||
|             qs = User.objects.filter(password=f"ipbased${ip}") | ||||
|             if qs.exists(): | ||||
|                 login(request, qs.get()) | ||||
|                 session = request.session | ||||
|                 session["permission_mask"] = 42 | ||||
|                 session.save() | ||||
|  | ||||
|         return self.get_response(request) | ||||
|  | ||||
|  | ||||
| class TurbolinksMiddleware(object): | ||||
|     """ | ||||
|     Send the `Turbolinks-Location` header in response to a visit that was redirected, | ||||
|   | ||||
| @@ -49,16 +49,6 @@ try: | ||||
| except ImportError: | ||||
|     pass | ||||
|  | ||||
| if "cas_server" in INSTALLED_APPS: | ||||
|     # CAS Settings | ||||
|     CAS_AUTO_CREATE_USER = False | ||||
|     CAS_LOGO_URL = "/static/img/Saperlistpopette.png" | ||||
|     CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png" | ||||
|     CAS_SHOW_POWERED = False | ||||
|  | ||||
| if "logs" in INSTALLED_APPS: | ||||
|     MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) | ||||
|  | ||||
| if DEBUG: | ||||
|     PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor'] | ||||
|     if "debug_toolbar" in INSTALLED_APPS: | ||||
|   | ||||
| @@ -35,8 +35,10 @@ INSTALLED_APPS = [ | ||||
|     'mailer', | ||||
|     'phonenumber_field', | ||||
|     'polymorphic', | ||||
|     'oauth2_provider', | ||||
|  | ||||
|     # Django contrib | ||||
|     # Django Admin will autodiscover our apps for our custom admin site. | ||||
|     'django.contrib.admin', | ||||
|     'django.contrib.admindocs', | ||||
|     'django.contrib.auth', | ||||
| @@ -77,6 +79,8 @@ MIDDLEWARE = [ | ||||
|     'django.middleware.locale.LocaleMiddleware', | ||||
|     'django.contrib.sites.middleware.CurrentSiteMiddleware', | ||||
|     'django_htcpcp_tea.middleware.HTCPCPTeaMiddleware', | ||||
|     'note_kfet.middlewares.SessionMiddleware', | ||||
|     'note_kfet.middlewares.LoginByIPMiddleware', | ||||
|     'note_kfet.middlewares.TurbolinksMiddleware', | ||||
| ] | ||||
|  | ||||
| @@ -154,6 +158,7 @@ from django.utils.translation import gettext_lazy as _ | ||||
| LANGUAGES = [ | ||||
|     ('de', _('German')), | ||||
|     ('en', _('English')), | ||||
|     ('es', _('Spanish')), | ||||
|     ('fr', _('French')), | ||||
| ] | ||||
|  | ||||
| @@ -213,6 +218,16 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None) | ||||
| SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com") | ||||
| DEFAULT_FROM_EMAIL = "NoteKfet2020 <" + SERVER_EMAIL + ">" | ||||
|  | ||||
| # Cache | ||||
| # https://docs.djangoproject.com/en/2.2/topics/cache/#setting-up-the-cache | ||||
| cache_address = os.getenv("CACHE_ADDRESS", "127.0.0.1:11211") | ||||
| CACHES = { | ||||
|     'default': { | ||||
|         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', | ||||
|         'LOCATION': cache_address, | ||||
|     } | ||||
| } | ||||
|  | ||||
| # Django REST Framework | ||||
| REST_FRAMEWORK = { | ||||
|     'DEFAULT_PERMISSION_CLASSES': [ | ||||
| @@ -232,7 +247,7 @@ REST_FRAMEWORK = { | ||||
| FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' | ||||
|  | ||||
| # After login redirect user to transfer page | ||||
| LOGIN_REDIRECT_URL = '/note/transfer/' | ||||
| LOGIN_REDIRECT_URL = '/' | ||||
|  | ||||
| # An user session will expired after 3 hours | ||||
| SESSION_COOKIE_AGE = 60 * 60 * 3 | ||||
|   | ||||
| @@ -24,6 +24,14 @@ if os.getenv("DJANGO_DEV_STORE_METHOD", "sqlite") != "postgresql": | ||||
|         } | ||||
|     } | ||||
|  | ||||
| # Dummy cache for development | ||||
| # https://docs.djangoproject.com/en/2.2/topics/cache/#setting-up-the-cache | ||||
| CACHES = { | ||||
|     'default': { | ||||
|         'BACKEND': 'django.core.cache.backends.dummy.DummyCache', | ||||
|     } | ||||
| } | ||||
|  | ||||
| # Break it, fix it! | ||||
| DEBUG = True | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| # CAS | ||||
| OPTIONAL_APPS = [ | ||||
| #    'cas_server', | ||||
| #    'debug_toolbar' | ||||
|     # 'cas_server', | ||||
|     # 'debug_toolbar', | ||||
|     # 'django_extensions', | ||||
| ] | ||||
|  | ||||
| # When a server error occured, send an email to these addresses | ||||
| # When a server error occurred, send an email to these addresses | ||||
| ADMINS = ( | ||||
|     ('Note Kfet', 'notekfet@example.com'), | ||||
| ) | ||||
|   | ||||
| @@ -22,6 +22,11 @@ | ||||
|     border-bottom-color: rgba(0, 0, 0, .250); | ||||
| } | ||||
|  | ||||
| /* Fixed width picture column */ | ||||
| .picture-col { | ||||
|     max-width: 202px; | ||||
| } | ||||
|  | ||||
| /* Limit fluid container to a max size */ | ||||
| .container-fluid { | ||||
|     max-width: 1600px; | ||||
|   | ||||
| @@ -363,8 +363,7 @@ function de_validate (id, validated, resourcetype) { | ||||
|       const errObj = JSON.parse(err.responseText) | ||||
|       let error = errObj.detail ? errObj.detail : errObj.non_field_errors | ||||
|       if (!error) { error = err.responseText } | ||||
|       addMsg('Une erreur est survenue lors de la validation/dévalidation ' + | ||||
|                 'de cette transaction : ' + error, 'danger') | ||||
|       addMsg(gettext('An error occured while (in)validating this transaction:') + ' ' + error, 'danger') | ||||
|  | ||||
|       refreshBalance() | ||||
|       // error if this method doesn't exist. Please define it. | ||||
|   | ||||
							
								
								
									
										134
									
								
								note_kfet/static/js/jsi18n/_default.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								note_kfet/static/js/jsi18n/_default.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| /* | ||||
| * You should never see this file. | ||||
| * It is here only for compatibility reasons in case of the command `compilejsmessages` was never executed. | ||||
| * Please execute this command to generate translation strings. | ||||
| */ | ||||
|  | ||||
| (function(globals) { | ||||
|  | ||||
|   var django = globals.django || (globals.django = {}); | ||||
|  | ||||
|  | ||||
|   django.pluralidx = function(n) { | ||||
|     var v=(n != 1); | ||||
|     if (typeof(v) == 'boolean') { | ||||
|       return v ? 1 : 0; | ||||
|     } else { | ||||
|       return v; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|  | ||||
|   /* gettext library */ | ||||
|  | ||||
|   django.catalog = django.catalog || {}; | ||||
|  | ||||
|  | ||||
|   if (!django.jsi18n_initialized) { | ||||
|     django.gettext = function(msgid) { | ||||
|       var value = django.catalog[msgid]; | ||||
|       if (typeof(value) == 'undefined') { | ||||
|         return msgid; | ||||
|       } else { | ||||
|         return (typeof(value) == 'string') ? value : value[0]; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     django.ngettext = function(singular, plural, count) { | ||||
|       var value = django.catalog[singular]; | ||||
|       if (typeof(value) == 'undefined') { | ||||
|         return (count == 1) ? singular : plural; | ||||
|       } else { | ||||
|         return value.constructor === Array ? value[django.pluralidx(count)] : value; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     django.gettext_noop = function(msgid) { return msgid; }; | ||||
|  | ||||
|     django.pgettext = function(context, msgid) { | ||||
|       var value = django.gettext(context + '\x04' + msgid); | ||||
|       if (value.indexOf('\x04') != -1) { | ||||
|         value = msgid; | ||||
|       } | ||||
|       return value; | ||||
|     }; | ||||
|  | ||||
|     django.npgettext = function(context, singular, plural, count) { | ||||
|       var value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); | ||||
|       if (value.indexOf('\x04') != -1) { | ||||
|         value = django.ngettext(singular, plural, count); | ||||
|       } | ||||
|       return value; | ||||
|     }; | ||||
|  | ||||
|     django.interpolate = function(fmt, obj, named) { | ||||
|       if (named) { | ||||
|         return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); | ||||
|       } else { | ||||
|         return fmt.replace(/%s/g, function(match){return String(obj.shift())}); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     /* formatting library */ | ||||
|  | ||||
|     django.formats = { | ||||
|     "DATETIME_FORMAT": "j \\d\\e F \\d\\e Y \\a \\l\\a\\s H:i", | ||||
|     "DATETIME_INPUT_FORMATS": [ | ||||
|       "%d/%m/%Y %H:%M:%S", | ||||
|       "%d/%m/%Y %H:%M:%S.%f", | ||||
|       "%d/%m/%Y %H:%M", | ||||
|       "%d/%m/%y %H:%M:%S", | ||||
|       "%d/%m/%y %H:%M:%S.%f", | ||||
|       "%d/%m/%y %H:%M", | ||||
|       "%Y-%m-%d %H:%M:%S", | ||||
|       "%Y-%m-%d %H:%M:%S.%f", | ||||
|       "%Y-%m-%d %H:%M", | ||||
|       "%Y-%m-%d" | ||||
|     ], | ||||
|     "DATE_FORMAT": "j \\d\\e F \\d\\e Y", | ||||
|     "DATE_INPUT_FORMATS": [ | ||||
|       "%d/%m/%Y", | ||||
|       "%d/%m/%y", | ||||
|       "%Y-%m-%d" | ||||
|     ], | ||||
|     "DECIMAL_SEPARATOR": ",", | ||||
|     "FIRST_DAY_OF_WEEK": 1, | ||||
|     "MONTH_DAY_FORMAT": "j \\d\\e F", | ||||
|     "NUMBER_GROUPING": 3, | ||||
|     "SHORT_DATETIME_FORMAT": "d/m/Y H:i", | ||||
|     "SHORT_DATE_FORMAT": "d/m/Y", | ||||
|     "THOUSAND_SEPARATOR": ".", | ||||
|     "TIME_FORMAT": "H:i", | ||||
|     "TIME_INPUT_FORMATS": [ | ||||
|       "%H:%M:%S", | ||||
|       "%H:%M:%S.%f", | ||||
|       "%H:%M" | ||||
|     ], | ||||
|     "YEAR_MONTH_FORMAT": "F \\d\\e Y" | ||||
|   }; | ||||
|  | ||||
|     django.get_format = function(format_type) { | ||||
|       var value = django.formats[format_type]; | ||||
|       if (typeof(value) == 'undefined') { | ||||
|         return format_type; | ||||
|       } else { | ||||
|         return value; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     /* add to global namespace */ | ||||
|     globals.pluralidx = django.pluralidx; | ||||
|     globals.gettext = django.gettext; | ||||
|     globals.ngettext = django.ngettext; | ||||
|     globals.gettext_noop = django.gettext_noop; | ||||
|     globals.pgettext = django.pgettext; | ||||
|     globals.npgettext = django.npgettext; | ||||
|     globals.interpolate = django.interpolate; | ||||
|     globals.get_format = django.get_format; | ||||
|  | ||||
|     django.jsi18n_initialized = true; | ||||
|   } | ||||
|  | ||||
| }(this)); | ||||
|  | ||||
							
								
								
									
										1
									
								
								note_kfet/static/js/jsi18n/de.js
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								note_kfet/static/js/jsi18n/de.js
									
									
									
									
									
										Symbolic link
									
								
							| @@ -0,0 +1 @@ | ||||
| _default.js | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user