1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 09:58:23 +02:00

Compare commits

..

26 Commits

Author SHA1 Message Date
3eed93e346 Remove unused file 2021-02-23 23:38:54 +01:00
4da523a1ba Merge branch 'faster_ci' of https://gitlab.crans.org/bde/nk20 into faster_ci 2021-02-23 23:38:26 +01:00
e74ff54468 please use the configuration I have written for hadolint 2021-02-23 22:23:16 +00:00
2e49c9ffbd Add CI docker linter for CI Dockerfiles 2021-02-23 22:23:16 +00:00
d20a1038a8 Add CI docker linter for nk20 Dockerfile 2021-02-23 22:23:16 +00:00
f6b711bb1b Add hadolint configuration file 2021-02-23 22:23:16 +00:00
893d87a9e1 Add ansible linting to the CI 2021-02-23 22:23:16 +00:00
9f3323c73e Add docker image for ansible lint to be used in CI 2021-02-23 22:23:16 +00:00
c57f81b920 Add skip list for ansible-lint 2021-02-23 22:23:16 +00:00
0636d84286 Add docker image for tox linting to be used in CI 2021-02-23 22:23:16 +00:00
ed06901fae fix typo (added image: twice) 2021-02-23 22:23:16 +00:00
28932f316b copy paste is a bad practice 2021-02-23 22:23:16 +00:00
9b50ba722c Add custom pre-built docker images to be used for the CI 2021-02-23 22:23:16 +00:00
3e3e61d23f Use prebuilt docker images in the CI 2021-02-23 22:23:16 +00:00
1129815ca3 please use the configuration I have written for hadolint 2021-02-23 23:22:51 +01:00
c13172d3ff Add CI docker linter for CI Dockerfiles 2021-02-23 23:14:35 +01:00
fcc4121225 Add CI docker linter for nk20 Dockerfile 2021-02-23 23:14:00 +01:00
a06f355559 Add hadolint configuration file 2021-02-23 23:10:30 +01:00
08df5fcccd Add ansible linting to the CI 2021-02-23 23:02:51 +01:00
b6c0f9758d Add docker image for ansible lint to be used in CI 2021-02-23 23:02:29 +01:00
a23093851f Add skip list for ansible-lint 2021-02-23 22:57:33 +01:00
d803ab5ec2 Add docker image for tox linting to be used in CI 2021-02-22 00:17:49 +01:00
d7a537b6b5 fix typo (added image: twice) 2021-02-21 23:52:42 +01:00
0941ee954d copy paste is a bad practice 2021-02-21 23:46:20 +01:00
fd11d96d95 Add custom pre-built docker images to be used for the CI 2021-02-21 23:40:03 +01:00
4bfc057454 Use prebuilt docker images in the CI 2021-02-21 23:39:08 +01:00
267 changed files with 3770 additions and 11032 deletions

3
.ansible-lint Normal file
View File

@ -0,0 +1,3 @@
skip_list:
- command-instead-of-shell # Use shell only when shell functionality is required
- experimental # all rules tagged as experimental

View File

@ -10,6 +10,7 @@ DJANGO_SECRET_KEY=CHANGE_ME
DJANGO_SETTINGS_MODULE=note_kfet.settings DJANGO_SETTINGS_MODULE=note_kfet.settings
CONTACT_EMAIL=tresorerie.bde@localhost CONTACT_EMAIL=tresorerie.bde@localhost
NOTE_URL=localhost NOTE_URL=localhost
DOMAIN=localhost
# Config for mails. Only used in production # Config for mails. Only used in production
NOTE_MAIL=notekfet@localhost NOTE_MAIL=notekfet@localhost

2
.gitignore vendored
View File

@ -42,13 +42,11 @@ map.json
backups/ backups/
/static/ /static/
/media/ /media/
/tmp/
# Virtualenv # Virtualenv
env/ env/
venv/ venv/
db.sqlite3 db.sqlite3
shell.nix
# ansibles customs host # ansibles customs host
ansible/host_vars/*.yaml ansible/host_vars/*.yaml

View File

@ -8,55 +8,27 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Debian Buster # Debian Buster
# py37-django22: py37-django22:
# stage: test stage: test
# image: debian:buster-backports image: otthorn/nk20_ci_37
# before_script: script: tox -e py37-django22
# - >
# apt-get update &&
# apt-get install --no-install-recommends -t buster-backports -y
# python3-django python3-django-crispy-forms
# python3-django-extensions python3-django-filters python3-django-polymorphic
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
# python3-bs4 python3-setuptools tox texlive-xetex
# script: tox -e py37-django22
# Ubuntu 20.04 # Ubuntu 20.04
py38-django22: py38-django22:
stage: test stage: test
image: ubuntu:20.04 image: otthorn/nk20_ci_38
before_script:
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
- >
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 py38-django22 script: tox -e py38-django22
# Debian Bullseye # Debian Bullseye
py39-django22: py39-django22:
stage: test stage: test
image: debian:bullseye image: otthorn/nk20_ci_39
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 script: tox -e py39-django22
# Tox linter
linters: linters:
stage: quality-assurance stage: quality-assurance
image: debian:bullseye image: debian:buster-backports
before_script: before_script:
- apt-get update && apt-get install -y tox - apt-get update && apt-get install -y tox
script: tox -e linters script: tox -e linters
@ -64,6 +36,20 @@ linters:
# Be nice to new contributors, but please use `tox` # Be nice to new contributors, but please use `tox`
allow_failure: true allow_failure: true
# Ansible linter
ansible-linter:
stage: quality-assurance
image: otthorn/nk20_ci_ansiblelint
script: ansible-lint ansible/
# Docker linter
docker-linter:
stage: quality-assurance
image: hadolint/hadolint
script:
- hadolint -c .hadolint Dockerfile
- hadolint -c .hadolint docker_ci/Dockerfile.*
# Compile documentation # Compile documentation
documentation: documentation:
stage: docs stage: docs

4
.hadolint Normal file
View File

@ -0,0 +1,4 @@
ignored:
- DL3008 # Do not force to pin version in apt (Debian)
- DL3013 # Do not force to pin version in pip (PyPI)
- DL3018 # Do not force to pin version in apk (Alpine)

View File

@ -1,8 +1,8 @@
# NoteKfet 2020 # NoteKfet 2020
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/main/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/main) [![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/main/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/main) [![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
## Table des matières ## Table des matières
@ -279,8 +279,7 @@ Le cahier des charges initial est disponible [sur le Wiki Crans](https://wiki.cr
La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django. La documentation des classes et fonctions est directement dans le code et est explorable à partir de la partie documentation de l'interface d'administration de Django.
**Commentez votre code !** **Commentez votre code !**
La documentation plus haut niveau sur le développement et sur l'utilisation La documentation plus haut niveau sur le développement est disponible sur [le Wiki associé au dépôt Git](https://gitlab.crans.org/bde/nk20/-/wikis/home).
est disponible sur <https://note.crans.org/doc> et également dans le dossier `docs`.
## FAQ ## FAQ

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)" prompt: "Password of the database (leave it blank to skip database init)"
private: yes private: yes
vars: vars:
mirror: eclats.crans.org mirror: mirror.crans.org
roles: roles:
- 1-apt-basic - 1-apt-basic
- 2-nk20 - 2-nk20

View File

@ -0,0 +1,6 @@
---
note:
server_name: note-beta.crans.org
git_branch: beta
cron_enabled: false
email: notekfet2020@lists.crans.org

View File

@ -2,6 +2,5 @@
note: note:
server_name: note-dev.crans.org server_name: note-dev.crans.org
git_branch: beta git_branch: beta
serve_static: false
cron_enabled: false cron_enabled: false
email: notekfet2020@lists.crans.org email: notekfet2020@lists.crans.org

View File

@ -1,7 +1,6 @@
--- ---
note: note:
server_name: note.crans.org server_name: note.crans.org
git_branch: main git_branch: master
serve_static: true
cron_enabled: true cron_enabled: true
email: notekfet2020@lists.crans.org email: notekfet2020@lists.crans.org

View File

@ -1,5 +1,6 @@
[dev] [dev]
bde-note-dev.adh.crans.org bde-note-dev.adh.crans.org
bde-nk20-beta.adh.crans.org
[prod] [prod]
bde-note.adh.crans.org bde-note.adh.crans.org

View File

@ -1,15 +1,14 @@
--- ---
- name: Add buster-backports to apt sources if needed - name: Add buster-backports to apt sources
apt_repository: apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main repo: deb http://{{ mirror }}/debian buster-backports main
state: present state: present
when: when: ansible_facts['distribution'] == "Debian"
- ansible_distribution == "Debian"
- ansible_distribution_major_version | int == 10
- name: Install note_kfet APT dependencies - name: Install note_kfet APT dependencies
apt: apt:
update_cache: true update_cache: true
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
install_recommends: false install_recommends: false
name: name:
# Common tools # Common tools

View File

@ -41,7 +41,6 @@ server {
# max upload size # max upload size
client_max_body_size 75M; # adjust to taste client_max_body_size 75M; # adjust to taste
{% if note.serve_static %}
# Django media # Django media
location /media { location /media {
alias /var/www/note_kfet/media; # your Django project's media files - amend as required alias /var/www/note_kfet/media; # your Django project's media files - amend as required
@ -51,7 +50,6 @@ server {
alias /var/www/note_kfet/static; # your Django project's static files - amend as required alias /var/www/note_kfet/static; # your Django project's static files - amend as required
} }
{% endif %}
location /doc { location /doc {
alias /var/www/documentation; # The documentation of the project alias /var/www/documentation; # The documentation of the project
} }

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig' default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -6,7 +6,7 @@
"name": "Pot", "name": "Pot",
"manage_entries": true, "manage_entries": true,
"can_invite": true, "can_invite": true,
"guest_entry_fee": 1000 "guest_entry_fee": 500
} }
}, },
{ {
@ -28,25 +28,5 @@
"can_invite": false, "can_invite": false,
"guest_entry_fee": 0 "guest_entry_fee": 0
} }
},
{
"model": "activity.activitytype",
"pk": 5,
"fields": {
"name": "Soir\u00e9e avec entrées",
"manage_entries": true,
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 7,
"fields": {
"name": "Soir\u00e9e avec invitations",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 0
}
} }
] ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta from datetime import timedelta
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
from member.models import Club from member.models import Club
from note.models import Note, NoteUser from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete, DateTimePickerInput from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models import Activity, Guest from .models import Activity, Guest
@ -24,16 +24,10 @@ class ActivityForm(forms.ModelForm):
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet") self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet" self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
clubs = list(Club.objects.filter(PermissionBackend clubs = list(Club.objects.filter(PermissionBackend
.filter_queryset(get_current_request(), Club, "view")).all()) .filter_queryset(get_current_authenticated_user(), Club, "view")).all())
shuffle(clubs) shuffle(clubs)
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
def clean_organizer(self):
organizer = self.cleaned_data['organizer']
if not organizer.note.is_active:
self.add_error('organiser', _('The note of this club is inactive.'))
return organizer
def clean_date_end(self): def clean_date_end(self):
date_end = self.cleaned_data["date_end"] date_end = self.cleaned_data["date_end"]
date_start = self.cleaned_data["date_start"] date_start = self.cleaned_data["date_start"]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-03-23 13:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('activity', '0002_auto_20200904_2341'),
]
operations = [
migrations.AlterField(
model_name='activity',
name='description',
field=models.TextField(blank=True, default='', verbose_name='description'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os
@ -66,8 +66,6 @@ class Activity(models.Model):
description = models.TextField( description = models.TextField(
verbose_name=_('description'), verbose_name=_('description'),
blank=True,
default="",
) )
location = models.CharField( location = models.CharField(
@ -125,14 +123,6 @@ class Activity(models.Model):
verbose_name=_('open'), verbose_name=_('open'),
) )
class Meta:
verbose_name = _("activity")
verbose_name_plural = _("activities")
unique_together = ("name", "date_start", "date_end",)
def __str__(self):
return self.name
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
@ -154,6 +144,14 @@ class Activity(models.Model):
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities() if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
return ret return ret
def __str__(self):
return self.name
class Meta:
verbose_name = _("activity")
verbose_name_plural = _("activities")
unique_together = ("name", "date_start", "date_end",)
class Entry(models.Model): class Entry(models.Model):
""" """
@ -254,13 +252,14 @@ class Guest(models.Model):
verbose_name=_("inviter"), verbose_name=_("inviter"),
) )
class Meta: @property
verbose_name = _("guest") def has_entry(self):
verbose_name_plural = _("guests") try:
unique_together = ("activity", "last_name", "first_name", ) if self.entry:
return True
def __str__(self): return False
return self.first_name + " " + self.last_name except AttributeError:
return False
@transaction.atomic @transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None): def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
@ -291,14 +290,13 @@ class Guest(models.Model):
return super().save(force_insert, force_update, using, update_fields) return super().save(force_insert, force_update, using, update_fields)
@property def __str__(self):
def has_entry(self): return self.first_name + " " + self.last_name
try:
if self.entry: class Meta:
return True verbose_name = _("guest")
return False verbose_name_plural = _("guests")
except AttributeError: unique_together = ("activity", "last_name", "first_name", )
return False
class GuestTransaction(Transaction): class GuestTransaction(Transaction):

View File

@ -1,9 +1,7 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
from django_tables2 import A from django_tables2 import A
@ -54,8 +52,8 @@ class GuestTable(tables.Table):
def render_entry(self, record): def render_entry(self, record):
if record.has_entry: if record.has_entry:
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> ' return format_html('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize())) '{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
def get_row_class(record): def get_row_class(record):
@ -93,7 +91,7 @@ class EntryTable(tables.Table):
if hasattr(record, 'username'): if hasattr(record, 'username'):
username = record.username username = record.username
if username != value: if username != value:
return mark_safe(escape(value) + " <em>aka.</em> " + escape(username)) return format_html(value + " <em>aka.</em> " + username)
return value return value
def render_balance(self, value): def render_balance(self, value):

View File

@ -38,7 +38,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a> </a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ..."> <input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<button id="trigger" class="btn btn-secondary">Click me !</button>
<hr> <hr>
@ -64,46 +63,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
refreshBalance(); refreshBalance();
} }
function process_qrcode() { alias_obj.keyup(reloadTable);
let name = alias_obj.val();
$.get("/api/note/note?search=" + name + "&format=json").done(
function (res) {
let note = res.results[0];
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: note.id,
guest: null
}).done(function () {
addMsg(interpolate(gettext(
"Entry made for %s whose balance is %s €"),
[note.name, note.balance / 100]), "success", 4000);
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000);
});
}
alias_obj.keyup(function(event) {
let code = event.originalEvent.keyCode
if (65 <= code <= 122 || code === 13) {
debounce(reloadTable)()
}
if (code === 0)
process_qrcode();
});
$(document).ready(init); $(document).ready(init);
alias_obj2 = document.getElementById("alias");
$("#trigger").click(function (e) {
addMsg("Clicked", "success", 1000);
alias_obj.val(alias_obj.val() + "\0");
alias_obj2.dispatchEvent(new KeyboardEvent('keyup'));
})
function init() { function init() {
$(".table-row").click(function (e) { $(".table-row").click(function (e) {
let target = e.target.parentElement; let target = e.target.parentElement;
@ -200,4 +163,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
}); });
} }
</script> </script>
{% endblock %} {% endblock %}

View File

@ -17,27 +17,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrajavascript %}
<script>
var date_end = document.getElementById("id_date_end");
var date_start = document.getElementById("id_date_start");
function update_date_end (){
if(date_end.value=="" || date_end.value<date_start.value){
date_end.value = date_start.value;
};
};
function update_date_start (){
if(date_start.value=="" || date_end.value<date_start.value){
date_start.value = date_end.value;
};
};
date_start.addEventListener('focusout', update_date_end);
date_end.addEventListener('focusout', update_date_start);
</script>
{% endblock %}

View File

@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
</h3> </h3>
{% render_table table %} {% render_table table %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta from datetime import timedelta

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5 from hashlib import md5
@ -17,8 +17,7 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView from django.views.generic import DetailView, TemplateView, UpdateView
from django.views.generic.list import ListView from django_tables2.views import SingleTableView
from django_tables2.views import MultiTableMixin
from note.models import Alias, NoteSpecial, NoteUser from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -58,42 +57,30 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
Displays all Activities, and classify if they are on-going or upcoming ones. Displays all Activities, and classify if they are on-going or upcoming ones.
""" """
model = Activity model = Activity
tables = [ActivityTable, ActivityTable] table_class = ActivityTable
ordering = ('-date_start',)
extra_context = {"title": _("Activities")} extra_context = {"title": _("Activities")}
def get_queryset(self, **kwargs): def get_queryset(self):
return super().get_queryset(**kwargs).distinct() return super().get_queryset().distinct()
def get_tables(self):
tables = super().get_tables()
tables[0].prefix = "all-"
tables[1].prefix = "upcoming-"
return tables
def get_tables_data(self):
# first table = all activities, second table = upcoming
return [
self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct()
.order_by("date_start")
]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
tables = context["tables"] upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
for name, table in zip(["table", "upcoming"], tables): context['upcoming'] = ActivityTable(
context[name] = table data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
prefix='upcoming-',
)
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all() started_activities = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.filter(open=True, valid=True).all()
context["started_activities"] = started_activities context["started_activities"] = started_activities
return context return context
@ -111,7 +98,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data() context = super().get_context_data()
table = GuestTable(data=Guest.objects.filter(activity=self.object) table = GuestTable(data=Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))) .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
context["guests"] = table context["guests"] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start) context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
@ -157,15 +144,15 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\ form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.filter(pk=self.kwargs["pk"]).first() .get(pk=self.kwargs["pk"])
form.fields["inviter"].initial = self.request.user.note form.fields["inviter"].initial = self.request.user.note
return form return form
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.activity = Activity.objects\ form.instance.activity = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"]) .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
@ -183,13 +170,10 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself), Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
it is closed or doesn't manage entries. it is closed or doesn't manage entries.
""" """
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"]) activity = Activity.objects.get(pk=self.kwargs["pk"])
sample_entry = Entry(activity=activity, note=self.request.user.note) sample_entry = Entry(activity=activity, note=self.request.user.note)
if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry): if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity.")) raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
if not activity.activity_type.manage_entries: if not activity.activity_type.manage_entries:
@ -207,8 +191,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
guest_qs = Guest.objects\ guest_qs = Guest.objects\
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
.filter(activity=activity)\ .filter(activity=activity)\
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
.order_by('last_name', 'first_name') .order_by('last_name', 'first_name').distinct()
if "search" in self.request.GET and self.request.GET["search"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -222,7 +206,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
) )
else: else:
guest_qs = guest_qs.none() guest_qs = guest_qs.none()
return guest_qs.distinct() return guest_qs
def get_invited_note(self, activity): def get_invited_note(self, activity):
""" """
@ -246,7 +230,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
) )
# Filter with permission backend # Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")) note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
if "search" in self.request.GET and self.request.GET["search"]: if "search" in self.request.GET and self.request.GET["search"]:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
@ -272,7 +256,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\ activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"]) .distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity context["activity"] = activity
@ -297,9 +281,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
activities_open = Activity.objects.filter(open=True).filter( activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all() PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request, if PermissionBackend.check_perm(self.request.user,
"activity.add_entry", "activity.add_entry",
Entry(activity=a, note=self.request.user.note,))] Entry(activity=a, note=self.request.user.note,))]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig' default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,5 +0,0 @@
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

View File

@ -1,20 +1,13 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer
from member.models import Membership
from note.api.serializers import NoteSerializer
from note.models import Alias
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
class UserSerializer(serializers.ModelSerializer): class UserSerializer(ModelSerializer):
""" """
REST API Serializer for Users. REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API. The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -29,7 +22,7 @@ class UserSerializer(serializers.ModelSerializer):
) )
class ContentTypeSerializer(serializers.ModelSerializer): class ContentTypeSerializer(ModelSerializer):
""" """
REST API Serializer for Users. REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API. The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
@ -38,54 +31,3 @@ class ContentTypeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ContentType model = ContentType
fields = '__all__' fields = '__all__'
class OAuthSerializer(serializers.ModelSerializer):
"""
Informations that are transmitted by OAuth.
For now, this includes user, profile and valid memberships.
This should be better managed later.
"""
normalized_name = serializers.SerializerMethodField()
profile = serializers.SerializerMethodField()
note = serializers.SerializerMethodField()
memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj):
return Alias.normalize(obj.username)
def get_profile(self, obj):
# Display the profile of the user only if we have rights to see it.
return ProfileSerializer().to_representation(obj.profile) \
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
def get_note(self, obj):
# Display the note of the user only if we have rights to see it.
return NoteSerializer().to_representation(obj.note) \
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
def get_memberships(self, obj):
# Display only memberships that we are allowed to see.
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
class Meta:
model = User
fields = (
'id',
'username',
'normalized_name',
'first_name',
'last_name',
'email',
'is_superuser',
'is_active',
'is_staff',
'profile',
'note',
'memberships',
)

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json import json
from datetime import datetime, date from datetime import datetime, date
from decimal import Decimal
from urllib.parse import quote_plus from urllib.parse import quote_plus
from warnings import warn from warnings import warn
@ -153,8 +152,6 @@ class TestAPI(TestCase):
value = value.isoformat() value = value.isoformat()
elif isinstance(value, ImageFieldFile): elif isinstance(value, ImageFieldFile):
value = value.name value = value.name
elif isinstance(value, Decimal):
value = str(value)
query = json.dumps({field.name: value}) query = json.dumps({field.name: value})
# Create sample permission # Create sample permission

View File

@ -1,11 +1,10 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
from django.conf.urls import url, include from django.conf.urls import url, include
from rest_framework import routers from rest_framework import routers
from .views import UserInformationView
from .viewsets import ContentTypeViewSet, UserViewSet from .viewsets import ContentTypeViewSet, UserViewSet
# Routers provide an easy way of automatically determining the URL conf. # Routers provide an easy way of automatically determining the URL conf.
@ -48,6 +47,5 @@ app_name = 'api'
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url('^', include(router.urls)), url('^', include(router.urls)),
url('^me/', UserInformationView.as_view()),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
] ]

View File

@ -1,20 +0,0 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from rest_framework.generics import RetrieveAPIView
from .serializers import OAuthSerializer
class UserInformationView(RetrieveAPIView):
"""
These fields are give to OAuth authenticators.
"""
serializer_class = OAuthSerializer
def get_queryset(self):
return User.objects.filter(pk=self.request.user.pk)
def get_object(self):
return self.request.user

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -9,6 +9,7 @@ from django.contrib.auth.models import User
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_session
from note.models import Alias from note.models import Alias
from .serializers import UserSerializer, ContentTypeSerializer from .serializers import UserSerializer, ContentTypeSerializer
@ -24,7 +25,9 @@ class ReadProtectedModelViewSet(ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct() user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet): class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
@ -37,7 +40,9 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self): def get_queryset(self):
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct() user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
class UserViewSet(ReadProtectedModelViewSet): class UserViewSet(ReadProtectedModelViewSet):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig' default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
@ -76,6 +76,9 @@ class Changelog(models.Model):
verbose_name=_('timestamp'), verbose_name=_('timestamp'),
) )
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))
class Meta: class Meta:
verbose_name = _("changelog") verbose_name = _("changelog")
verbose_name_plural = _("changelogs") verbose_name_plural = _("changelogs")
@ -83,6 +86,3 @@ class Changelog(models.Model):
def __str__(self): def __str__(self):
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format( return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp)) action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
def delete(self, using=None, keep_parents=False):
raise ValidationError(_("Logs cannot be destroyed."))

View File

@ -1,11 +1,11 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from note.models import NoteUser, Alias from note.models import NoteUser, Alias
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
from .models import Changelog from .models import Changelog
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
previous = instance._previous previous = instance._previous
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request() user, ip = get_current_authenticated_user(), get_current_ip()
if request is None: if user is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -71,23 +71,9 @@ def save_object(sender, instance, **kwargs):
# else: # else:
if note.exists(): if note.exists():
user = note.get().user user = note.get().user
else:
user = None
else:
user = request.user
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')
if not user.is_authenticated:
# For registration and OAuth2 purposes
user = None
# noinspection PyProtectedMember # noinspection PyProtectedMember
if request is not None and instance._meta.label_lower == "auth.user" and previous: if user is not None and instance._meta.label_lower == "auth.user" and previous:
# On n'enregistre pas les connexions # On n'enregistre pas les connexions
if instance.last_login != previous.last_login: if instance.last_login != previous.last_login:
return return
@ -135,9 +121,9 @@ def delete_object(sender, instance, **kwargs):
return return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request() user, ip = get_current_authenticated_user(), get_current_ip()
if request is None: if user is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py` # Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée # On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info # IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
@ -149,20 +135,6 @@ def delete_object(sender, instance, **kwargs):
# else: # else:
if note.exists(): if note.exists():
user = note.get().user user = note.get().user
else:
user = None
else:
user = request.user
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')
if not user.is_authenticated:
# For registration and OAuth2 purposes
user = None
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles # On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
class CustomSerializer(ModelSerializer): class CustomSerializer(ModelSerializer):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig' default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,17 +0,0 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
from note.models import Alias
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["normalized_name"] = Alias.normalize(self.user.username)
return d

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import io import io
@ -47,13 +47,6 @@ class ProfileForm(forms.ModelForm):
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date")) last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
VSS_charter_read = forms.BooleanField(
required=True,
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
help_text=_("Tick after having read and accepted the anti-VSS charter \
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
)
def clean_promotion(self): def clean_promotion(self):
promotion = self.cleaned_data["promotion"] promotion = self.cleaned_data["promotion"]
if promotion > timezone.now().year: if promotion > timezone.now().year:
@ -121,7 +114,7 @@ class ImageForm(forms.Form):
frame = frame.crop((x, y, x + w, y + h)) frame = frame.crop((x, y, x + w, y + h))
frame = frame.resize( frame = frame.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH), (settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.LANCZOS, Image.ANTIALIAS,
) )
frames.append(frame) frames.append(frame)
@ -138,9 +131,6 @@ class ImageForm(forms.Form):
return cleaned_data return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):
def clean(self): def clean(self):
@ -154,7 +144,7 @@ class ClubForm(forms.ModelForm):
class Meta: class Meta:
model = Club model = Club
exclude = ("add_registration_form",) fields = '__all__'
widgets = { widgets = {
"membership_fee_paid": AmountInput(), "membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(), "membership_fee_unpaid": AmountInput(),

View File

@ -1,14 +1,12 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import hashlib import hashlib
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import PBKDF2PasswordHasher, mask_hash from django.contrib.auth.hashers import PBKDF2PasswordHasher
from django.utils.crypto import constant_time_compare from django.utils.crypto import constant_time_compare
from django.utils.translation import gettext_lazy as _ from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from note_kfet.middlewares import get_current_request
class CustomNK15Hasher(PBKDF2PasswordHasher): class CustomNK15Hasher(PBKDF2PasswordHasher):
@ -26,22 +24,16 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
def must_update(self, encoded): def must_update(self, encoded):
if settings.DEBUG: if settings.DEBUG:
# Small hack to let superusers to impersonate people. current_user = get_current_authenticated_user()
# Don't change their password.
request = get_current_request()
current_user = request.user
if current_user is not None and current_user.is_superuser: if current_user is not None and current_user.is_superuser:
return False return False
return True return True
def verify(self, password, encoded): def verify(self, password, encoded):
if settings.DEBUG: if settings.DEBUG:
# Small hack to let superusers to impersonate people. current_user = get_current_authenticated_user()
# If a superuser is already connected, let him/her log in as another person.
request = get_current_request()
current_user = request.user
if current_user is not None and current_user.is_superuser\ if current_user is not None and current_user.is_superuser\
and request.session.get("permission_mask", -1) >= 42: and get_current_session().get("permission_mask", -1) >= 42:
return True return True
if '|' in encoded: if '|' in encoded:
@ -49,18 +41,6 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass) return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
return super().verify(password, encoded) return super().verify(password, encoded)
def safe_summary(self, encoded):
# Displayed information in Django Admin.
if '|' in encoded:
salt, db_hashed_pass = encoded.split('$')[2].split('|')
return OrderedDict([
(_('algorithm'), 'custom_nk15'),
(_('iterations'), '1'),
(_('salt'), mask_hash(salt)),
(_('hash'), mask_hash(db_hashed_pass)),
])
return super().safe_summary(encoded)
class DebugSuperuserBackdoor(PBKDF2PasswordHasher): class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
""" """
@ -71,11 +51,8 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
def verify(self, password, encoded): def verify(self, password, encoded):
if settings.DEBUG: if settings.DEBUG:
# Small hack to let superusers to impersonate people. current_user = get_current_authenticated_user()
# If a superuser is already connected, let him/her log in as another person.
request = get_current_request()
current_user = request.user
if current_user is not None and current_user.is_superuser\ if current_user is not None and current_user.is_superuser\
and request.session.get("permission_mask", -1) >= 42: and get_current_session().get("permission_mask", -1) >= 42:
return True return True
return super().verify(password, encoded) return super().verify(password, encoded)

View File

@ -19,8 +19,8 @@ def create_bde_and_kfet(apps, schema_editor):
membership_fee_paid=500, membership_fee_paid=500,
membership_fee_unpaid=500, membership_fee_unpaid=500,
membership_duration=396, membership_duration=396,
membership_start="2021-08-01", membership_start="2020-08-01",
membership_end="2022-09-30", membership_end="2021-09-30",
) )
Club.objects.get_or_create( Club.objects.get_or_create(
id=2, id=2,
@ -31,8 +31,8 @@ def create_bde_and_kfet(apps, schema_editor):
membership_fee_paid=3500, membership_fee_paid=3500,
membership_fee_unpaid=3500, membership_fee_unpaid=3500,
membership_duration=396, membership_duration=396,
membership_start="2021-08-01", membership_start="2020-08-01",
membership_end="2022-09-30", membership_end="2021-09-30",
) )
NoteClub.objects.get_or_create( NoteClub.objects.get_or_create(

View File

@ -1,23 +0,0 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0006_create_note_account_bde_membership'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='roles',
field=models.ManyToManyField(related_name='memberships', to='permission.Role', verbose_name='roles'),
),
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2021, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.24 on 2021-10-05 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0007_auto_20210313_1235'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='department',
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.26 on 2022-09-04 21:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0008_auto_20211005_1544'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2023-08-23 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0009_auto_20220904_2325'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2023-08-31 09:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0010_new_default_year'),
]
operations = [
migrations.AddField(
model_name='profile',
name='VSS_charter_read',
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-07-15 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0011_profile_vss_charter_read'),
]
operations = [
migrations.AddField(
model_name='club',
name='add_registration_form',
field=models.BooleanField(default=False, verbose_name='add to registration form'),
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime import datetime
@ -28,6 +28,7 @@ class Profile(models.Model):
We do not want to patch the Django Contrib :model:`auth.User`model; We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information. so this model add an user profile with additional information.
""" """
user = models.OneToOneField( user = models.OneToOneField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -56,7 +57,7 @@ class Profile(models.Model):
('A1', _("Mathematics (A1)")), ('A1', _("Mathematics (A1)")),
('A2', _("Physics (A2)")), ('A2', _("Physics (A2)")),
("A'2", _("Applied physics (A'2)")), ("A'2", _("Applied physics (A'2)")),
("A''2", _("Chemistry (A''2)")), ('A''2', _("Chemistry (A''2)")),
('A3', _("Biology (A3)")), ('A3', _("Biology (A3)")),
('B1234', _("SAPHIRE (B1234)")), ('B1234', _("SAPHIRE (B1234)")),
('B1', _("Mechanics (B1)")), ('B1', _("Mechanics (B1)")),
@ -73,7 +74,7 @@ class Profile(models.Model):
promotion = models.PositiveSmallIntegerField( promotion = models.PositiveSmallIntegerField(
null=True, null=True,
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1, default=datetime.date.today().year,
verbose_name=_("promotion"), verbose_name=_("promotion"),
help_text=_("Year of entry to the school (None if not ENS student)"), help_text=_("Year of entry to the school (None if not ENS student)"),
) )
@ -133,22 +134,6 @@ class Profile(models.Model):
default=False, default=False,
) )
VSS_charter_read = models.BooleanField(
verbose_name=_("VSS charter read"),
default=False
)
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
indexes = [models.Index(fields=['user'])]
def __str__(self):
return str(self.user)
def get_absolute_url(self):
return reverse('member:user_detail', args=(self.user_id,))
@property @property
def ens_year(self): def ens_year(self):
""" """
@ -173,6 +158,17 @@ class Profile(models.Model):
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists() return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
return False return False
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
indexes = [models.Index(fields=['user'])]
def get_absolute_url(self):
return reverse('member:user_detail', args=(self.user_id,))
def __str__(self):
return str(self.user)
def send_email_validation_link(self): def send_email_validation_link(self):
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account")) subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
token = email_validation_token.make_token(self.user) token = email_validation_token.make_token(self.user)
@ -204,11 +200,9 @@ class Club(models.Model):
max_length=255, max_length=255,
unique=True, unique=True,
) )
email = models.EmailField( email = models.EmailField(
verbose_name=_('email'), verbose_name=_('email'),
) )
parent_club = models.ForeignKey( parent_club = models.ForeignKey(
'self', 'self',
null=True, null=True,
@ -259,17 +253,23 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'), help_text=_('Maximal date of a membership, after which members must renew it.'),
) )
add_registration_form = models.BooleanField( def update_membership_dates(self):
verbose_name=_("add to registration form"), """
default=False, This function is called each time the club detail view is displayed.
) Update the year of the membership dates.
"""
if not self.membership_start:
return
class Meta: today = datetime.date.today()
verbose_name = _("club")
verbose_name_plural = _("clubs")
def __str__(self): if (today - self.membership_start).days >= 365:
return self.name self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
@transaction.atomic @transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
@ -282,29 +282,16 @@ class Club(models.Model):
self.membership_end = None self.membership_end = None
super().save(force_insert, force_update, update_fields) super().save(force_insert, force_update, update_fields)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,)) return reverse_lazy('member:club_detail', args=(self.pk,))
def update_membership_dates(self):
"""
This function is called each time the club detail view is displayed.
Update the year of the membership dates.
"""
if not self.membership_start or not self.membership_end:
return
today = datetime.date.today()
while (today - self.membership_start).days >= 365:
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
if self.membership_end:
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
class Membership(models.Model): class Membership(models.Model):
""" """
@ -344,66 +331,6 @@ class Membership(models.Model):
verbose_name=_('fee'), verbose_name=_('fee'),
) )
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
@transaction.atomic
def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
"""
# Ensure that club membership dates are valid
old_membership_start = self.club.membership_start
self.club.update_membership_dates()
if self.club.membership_start != old_membership_start:
self.club.save()
created = not self.pk
if not created:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
else:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.club.parent_club is not None:
# Check that the user is already a member of the parent club if the membership is created
if not Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
date_start__gte=self.club.parent_club.membership_start,
).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
self.renew_parent()
else:
raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name)
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
@property @property
def valid(self): def valid(self):
""" """
@ -481,6 +408,52 @@ class Membership(models.Model):
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all()) parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save() 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.
"""
created = not self.pk
if not created:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
else:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.club.parent_club is not None:
# Check that the user is already a member of the parent club if the membership is created
if not Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
date_start__gte=self.club.parent_club.membership_start,
).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
self.renew_parent()
else:
raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name)
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
def make_transaction(self): def make_transaction(self):
""" """
Create Membership transaction associated to this membership. Create Membership transaction associated to this membership.
@ -518,3 +491,11 @@ class Membership(models.Model):
soge_credit.save() soge_credit.save()
else: else:
transaction.save(force_insert=True) transaction.save(force_insert=True)
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,64 +0,0 @@
/**
* On form submit, create a new friendship
*/
function form_create_trust (e) {
// Do not submit HTML form
e.preventDefault()
// Get data and send to API
const formData = new FormData(e.target)
$.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
function (trusted_alias) {
if ((trusted_alias.note == formData.get('trusting')))
{
addMsg(gettext("You can't add yourself as a friend"), "danger")
return
}
create_trust(formData.get('trusting'), trusted_alias.note)
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* Create a trust between users
* @param trusting:Integer trusting note id
* @param trusted:Integer trusted note id
*/
function create_trust(trusting, trusted) {
$.post('/api/note/trust/', {
trusting: trusting,
trusted: trusted,
csrfmiddlewaretoken: CSRF_TOKEN
}).done(function () {
// Reload tables
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
addMsg(gettext('Friendship successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the trust
* @param button_id:Integer Trust id to remove
*/
function delete_button (button_id) {
$.ajax({
url: '/api/note/trust/' + button_id + '/',
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg(gettext('Friendship successfully deleted'), 'success')
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
$(document).ready(function () {
// Attach event
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
})

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date from datetime import date
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.html import format_html from django.utils.html import format_html
from note.templatetags.pretty_money import pretty_money from note.templatetags.pretty_money import pretty_money
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models import Club, Membership from .models import Club, Membership
@ -31,8 +31,7 @@ class ClubTable(tables.Table):
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'id': lambda record: "row-" + str(record.pk), 'id': lambda record: "row-" + str(record.pk),
'data-href': lambda record: record.pk, 'data-href': lambda record: record.pk
'style': 'cursor:pointer',
} }
@ -52,19 +51,19 @@ class UserTable(tables.Table):
def render_email(self, record, value): def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail # Replace the email by a dash if the user can't see the profile detail
# Replace also the URL # Replace also the URL
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile): if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
value = "" value = ""
record.email = value record.email = value
return value return value
def render_section(self, record, value): def render_section(self, record, value):
return value \ return value \
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \ if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
else "" else ""
def render_balance(self, record, value): def render_balance(self, record, value):
return pretty_money(value)\ return pretty_money(value)\
if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else "" if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
class Meta: class Meta:
attrs = { attrs = {
@ -75,8 +74,7 @@ class UserTable(tables.Table):
model = User model = User
row_attrs = { row_attrs = {
'class': 'table-row', 'class': 'table-row',
'data-href': lambda record: record.pk, 'data-href': lambda record: record.pk
'style': 'cursor:pointer',
} }
@ -95,7 +93,7 @@ class MembershipTable(tables.Table):
def render_user(self, value): def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail. # If the user has the right, link the displayed user with the page of its detail.
s = value.username s = value.username
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value): if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>", s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s) url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
@ -104,7 +102,7 @@ class MembershipTable(tables.Table):
def render_club(self, value): def render_club(self, value):
# If the user has the right, link the displayed club with the page of its detail. # If the user has the right, link the displayed club with the page of its detail.
s = value.name s = value.name
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value): if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>", s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s) url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
@ -120,7 +118,7 @@ class MembershipTable(tables.Table):
club=record.club, club=record.club,
user=record.user, user=record.user,
date_start__gte=record.club.membership_start, date_start__gte=record.club.membership_start,
date_end__lte=record.club.membership_end or date(9999, 12, 31), date_end__lte=record.club.membership_end,
).exists(): # If the renew is not yet performed ).exists(): # If the renew is not yet performed
empty_membership = Membership( empty_membership = Membership(
club=record.club, club=record.club,
@ -129,7 +127,7 @@ class MembershipTable(tables.Table):
date_end=date.today(), date_end=date.today(),
fee=0, fee=0,
) )
if PermissionBackend.check_perm(get_current_request(), 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', renew_url = reverse_lazy('member:club_renew_membership',
kwargs={"pk": record.pk}) kwargs={"pk": record.pk})
@ -144,7 +142,7 @@ class MembershipTable(tables.Table):
# If the user has the right to manage the roles, display the link to manage them # If the user has the right to manage the roles, display the link to manage them
roles = record.roles.all() roles = record.roles.all()
s = ", ".join(str(role) for role in roles) s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record): if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk})) s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+ "'>" + s + "</a>") + "'>" + s + "</a>")
return s return s
@ -167,7 +165,7 @@ class ClubManagerTable(tables.Table):
def render_user(self, value): def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail. # If the user has the right, link the displayed user with the page of its detail.
s = value.username s = value.username
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value): if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>", s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s) url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)

View File

@ -25,14 +25,6 @@
</a> </a>
</dd> </dd>
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
</a>
</dd>
{% if "member.view_profile"|has_perm:user_object.profile %} {% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd> <dd class="col-xl-6">{{ user_object.profile.section }}</dd>
@ -47,23 +39,20 @@
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd> <dd class="col-xl-6">{{ user_object.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt> {% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %}
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd> <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>
{% endif %}
{% endif %} {% endif %}
</dl> </dl>
{% if user_object.pk == user.pk %} {% if user_object.pk == user.pk %}
<div class="text-center"> <div class="text-center">
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}"> <a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
<i class="fa fa-cogs"></i>&nbsp;{% trans 'API token' %} <i class="fa fa-cogs"></i>{% trans 'API token' %}
</a>
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
<i class="fa fa-qrcode"></i>&nbsp;{% trans 'QR Code' %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -5,98 +5,32 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<div class="row mt-4"> <div class="alert alert-info">
<div class="col-xl-6"> <h4>À quoi sert un jeton d'authentification ?</h4>
<div class="card">
<div class="card-header text-center">
<h3>{% trans "Token authentication" %}</h3>
</div>
<div class="card-body">
<div class="alert alert-info">
<h4>À quoi sert un jeton d'authentification ?</h4>
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
depuis un client externe.<br /> Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code>
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code> pour pouvoir vous identifier.<br /><br />
pour pouvoir vous identifier.<br /><br />
La documentation de l'API est disponible ici : Une documentation de l'API arrivera ultérieurement.
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
</div>
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
{% else %}
<em>caché</em> (<a href="?show">montrer</a>)
{% endif %}
<br />
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
</div>
<div class="alert alert-warning">
<strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
</div>
</div>
<div class="card-footer text-center">
<a href="?regenerate">
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
</a>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card">
<div class="card-header text-center">
<h3>{% trans "OAuth2 authentication" %}</h3>
</div>
<div class="card-header">
<div class="alert alert-info">
<p>
La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
permettre à des applications tierces d'interagir avec la Note en récoltant des informations
(de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
</p>
<p>
L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
les droits dont on a besoin, en restreignant leur usage par jeton généré.
</p>
<p>
La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
<a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
</p>
</div>
Liste des URL à communiquer à votre application :
<ul>
<li>
{% trans "Authorization:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
</li>
<li>
{% trans "Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
</li>
<li>
{% trans "Revoke Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
</li>
<li>
{% trans "Introspect Token:" %}
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
</li>
</ul>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
</div>
</div>
</div>
</div> </div>
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
{% else %}
<em>caché</em> (<a href="?show">montrer</a>)
{% endif %}
<br />
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
</div>
<div class="alert alert-warning">
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
</div>
<a href="?regenerate">
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
</a>
{% endblock %} {% endblock %}

View File

@ -14,9 +14,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<form method="post" enctype="multipart/form-data" id="formUpload"> <form method="post" enctype="multipart/form-data" id="formUpload">
{% csrf_token %} {% csrf_token %}
{{ form |crispy }} {{ form |crispy }}
{% if user.note.display_image != "pic/default.png" %}
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
{% endif %}
</form> </form>
</div> </div>
<!-- MODAL TO CROP THE IMAGE --> <!-- MODAL TO CROP THE IMAGE -->

View File

@ -1,48 +0,0 @@
{% extends "member/base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load static django_tables2 i18n %}
{% block profile_content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Add friends" %}
</h3>
<div class="card-body">
{% if can_create %}
<form class="input-group" method="POST" id="form_trust">
{% csrf_token %}
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
{%include "autocomplete_model.html" %}
<div class="input-group-append">
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
</div>
</form>
{% endif %}
</div>
{% render_table trusting %}
</div>
<div class="alert alert-warning card mb-3">
{% blocktrans trimmed %}
Adding someone as a friend enables them to initiate transactions coming
from your account (while keeping your balance positive). This is
designed to simplify using note kfet transfers to transfer money between
users. The intent is that one person can make all transfers for a group of
friends without needing additional rights among them.
{% endblocktrans %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "People having you as a friend" %}
</h3>
{% render_table trusted_by %}
</div>
{% endblock %}
{% block extrajavascript %}
<script src="{% static "member/js/trust.js" %}"></script>
<script src="{% static "js/autocomplete_model.js" %}"></script>
{% endblock%}

View File

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<div class="card bg-light">
<h3 class="card-header text-center">
{% trans "QR Code for" %} {{ user_object.username }} ({{ user_object.first_name }} {{user_object.last_name }})
</h3>
<div class="text-center" id="qrcode">
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
var qrc = new QRCode(document.getElementById("qrcode"), {
text: "{{ user_object.pk }}\0",
width: 1024,
height: 1024
});
</script>
{% endblock %}
{% block extracss %}
<style>
img {
width: 100%
}
</style>
{% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date from datetime import date

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import hashlib import hashlib
@ -183,7 +183,7 @@ class TestMemberships(TestCase):
club = Club.objects.get(name="Kfet") club = Club.objects.get(name="Kfet")
else: else:
club = Club.objects.create( club = Club.objects.create(
name="Second club without BDE", name="Second club " + ("with BDE" if bde_parent else "without BDE"),
parent_club=None, parent_club=None,
email="newclub@example.com", email="newclub@example.com",
require_memberships=True, require_memberships=True,
@ -335,7 +335,6 @@ class TestMemberships(TestCase):
ml_sports_registration=True, ml_sports_registration=True,
ml_art_registration=True, ml_art_registration=True,
report_frequency=7, report_frequency=7,
VSS_charter_read=True
)) ))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200) self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.assertTrue(User.objects.filter(username="toto changed").exists()) self.assertTrue(User.objects.filter(username="toto changed").exists())

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path
@ -23,7 +23,5 @@ urlpatterns = [
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"), path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"), path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
path('user/<int:pk>/qr_code/', views.QRCodeView.as_view(), name='qr_code'),
] ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta, date from datetime import timedelta, date
@ -18,15 +18,15 @@ from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from note.models import Alias, NoteClub, NoteUser, Trust from note.models import Alias, NoteUser
from note.models.transactions import Transaction, SpecialTransaction from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable from note.tables import HistoryTable, AliasTable
from note_kfet.middlewares import _set_current_request from note_kfet.middlewares import _set_current_user_and_ip
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \ from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm,\
CustomAuthenticationForm, MembershipRolesForm CustomAuthenticationForm, MembershipRolesForm
from .models import Club, Membership from .models import Club, Membership
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
@ -41,8 +41,7 @@ class CustomLoginView(LoginView):
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
logout(self.request) logout(self.request)
self.request.user = form.get_user() _set_current_user_and_ip(form.get_user(), self.request.session, None)
_set_current_request(self.request)
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
return super().form_valid(form) return super().form_valid(form)
@ -71,7 +70,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.") form.fields['email'].help_text = _("This address must be valid.")
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile): 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, context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None) data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency: if not self.object.profile.report_frequency:
@ -154,13 +153,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
.order_by("-created_at")\ .order_by("-created_at")\
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
history_table = HistoryTable(history_list, prefix='transaction-') history_table = HistoryTable(history_list, prefix='transaction-')
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table context['history_list'] = history_table
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\ club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("club__name", "-date_start") .order_by("club__name", "-date_start")
# Display only the most recent membership # Display only the most recent membership
club_list = club_list.distinct("club__name")\ club_list = club_list.distinct("club__name")\
@ -174,23 +173,24 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note = NoteUser.objects.get(pk=user.note.pk) modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests # Don't log these tests
modified_note._no_signal = True modified_note._no_signal = True
modified_note.is_active = False modified_note.is_active = True
modified_note.inactivity_reason = 'manual' modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\ context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request.user, "note.change_noteuser_is_active",
modified_note)
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk) old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
modified_note.inactivity_reason = 'forced' modified_note.inactivity_reason = 'forced'
modified_note._force_save = True modified_note._force_save = True
modified_note.save() modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\ context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request.user, "note.change_note_is_active", modified_note)
old_note._force_save = True old_note._force_save = True
old_note._no_signal = True old_note._no_signal = True
old_note.save() old_note.save()
modified_note.refresh_from_db() modified_note.refresh_from_db()
modified_note.is_active = True modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\ context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request.user, "note.change_note_is_active", modified_note)
return context return context
@ -237,46 +237,12 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\ pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
.filter(profile__registration_valid=False) .filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists() context["can_manage_registrations"] = pre_registered_users.exists()
return context return context
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user trust relationships
"""
model = User
template_name = 'member/profile_trust.html'
context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["trusting"] = TrustTable(
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["trusted_by"] = TrustedTable(
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
))
context["widget"] = {
"name": "trusted",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "trusted",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
View and manage user aliases. View and manage user aliases.
@ -290,9 +256,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable( context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
.order_by('normalized_name').all()) context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
normalized_name="", normalized_name="",
@ -326,15 +291,12 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
"""Save image to note""" """Save image to note"""
image = form.cleaned_data['image'] image = form.cleaned_data['image']
if image is None: # Rename as a PNG or GIF
image = "pic/default.png" extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else: else:
# Rename as a PNG or GIF image.name = "{}_pic.png".format(self.object.note.pk)
extension = image.name.split(".")[-1]
if extension == "gif":
image.name = "{}_pic.gif".format(self.object.note.pk)
else:
image.name = "{}_pic.png".format(self.object.note.pk)
# Save # Save
self.object.note.display_image = image self.object.note.display_image = image
@ -368,14 +330,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
context['token'] = Token.objects.get_or_create(user=self.request.user)[0] context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
return context return context
class QRCodeView(LoginRequiredMixin, DetailView):
"""
Affiche le QR Code
"""
model = User
context_object_name = "user_object"
template_name = "member/qr_code.html"
extra_context = {"title": _("QR Code")}
# ******************************* # # ******************************* #
# CLUB # # CLUB #
@ -428,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club( context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
name="", name="",
email="club@example.com", email="club@example.com",
)) ))
@ -449,12 +403,9 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = self.object club = context["club"]
context["note"] = club.note if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates() club.update_membership_dates()
# managers list # 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())\ date_start__lte=date.today(), date_end__gte=date.today())\
@ -462,7 +413,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["managers"] = ClubManagerTable(data=managers, prefix="managers-") context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history # transaction history
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\ .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
.order_by('-created_at') .order_by('-created_at')
history_table = HistoryTable(club_transactions, prefix="history-") history_table = HistoryTable(club_transactions, prefix="history-")
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
@ -471,7 +422,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
club_member = Membership.objects.filter( club_member = Membership.objects.filter(
club=club, club=club,
date_end__gte=date.today() - timedelta(days=15), date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\ ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("user__username", "-date_start") .order_by("user__username", "-date_start")
# Display only the most recent membership # Display only the most recent membership
club_member = club_member.distinct("user__username")\ club_member = club_member.distinct("user__username")\
@ -492,29 +443,6 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_add_members"] = PermissionBackend()\ context["can_add_members"] = PermissionBackend()\
.has_perm(self.request.user, "member.add_membership", empty_membership) .has_perm(self.request.user, "member.add_membership", empty_membership)
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteClub.objects.get(pk=club.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_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
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
return context return context
@ -531,8 +459,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable(note.alias.filter( context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all()) PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
normalized_name="", normalized_name="",
@ -607,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form = context['form'] form = context['form']
if "club_pk" in self.kwargs: # We create a new membership. if "club_pk" in self.kwargs: # We create a new membership.
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["club_pk"], weiclub=None) .get(pk=self.kwargs["club_pk"], weiclub=None)
form.fields['credit_amount'].initial = club.membership_fee_paid form.fields['credit_amount'].initial = club.membership_fee_paid
# Ensure that the user is member of the parent club and all its the family tree. # Ensure that the user is member of the parent club and all its the family tree.
@ -697,6 +625,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# Retrieve form data # Retrieve form data
credit_type = form.cleaned_data["credit_type"] credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"] credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet") soge = form.cleaned_data["soge"] and not user.profile.soge and (club.name == "BDE" or club.name == "Kfet")
if not credit_type: if not credit_type:
@ -727,7 +658,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter( if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user, user=form.instance.user,
club=club.parent_club, club=club.parent_club,
date_start__gte=club.parent_club.membership_start, date_start__lte=timezone.now(),
date_end__gte=club.parent_club.membership_end,
).exists(): ).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name) form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True error = True
@ -742,9 +674,17 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
.format(form.instance.club.membership_end)) .format(form.instance.club.membership_end))
error = True error = True
if credit_amount and not SpecialTransaction.validate_payment_form(form): if credit_amount:
# Check that special information for payment are filled if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
error = True 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."))
error = True
return not error return not error
@ -755,7 +695,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
# Get the club that is concerned by the membership # Get the club that is concerned by the membership
if "club_pk" in self.kwargs: # get from url of new membership if "club_pk" in self.kwargs: # get from url of new membership
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"]) .get(pk=self.kwargs["club_pk"])
user = form.instance.user user = form.instance.user
old_membership = None old_membership = None
@ -764,10 +704,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = old_membership.club club = old_membership.club
user = old_membership.user user = old_membership.user
# Update club membership date
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates()
form.instance.club = club form.instance.club = club
# Get form data # Get form data
@ -810,7 +746,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
# When we renew the BDE membership, we update the profile section # When we renew the BDE membership, we update the profile section
# that should happens at least once a year. # that should happens at least once a year.
user.profile.section = user.profile.section_generated user.profile.section = user.profile.section_generated
user.profile._force_save = True
user.profile.save() user.profile.save()
# Credit note before the membership is created. # Credit note before the membership is created.
@ -943,7 +878,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = Club.objects.filter( club = Club.objects.filter(
PermissionBackend.filter_queryset(self.request, Club, "view") PermissionBackend.filter_queryset(self.request.user, Club, "view")
).get(pk=self.kwargs["pk"]) ).get(pk=self.kwargs["pk"])
context["club"] = club context["club"] = club

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig' default_app_config = 'note.apps.NoteConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site from note_kfet.admin import admin_site
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction, SpecialTransaction RecurrentTransaction, MembershipTransaction, SpecialTransaction
from .templatetags.pretty_money import pretty_money from .templatetags.pretty_money import pretty_money
@ -21,16 +21,6 @@ class AliasInlines(admin.TabularInline):
model = Alias model = Alias
class TrustInlines(admin.TabularInline):
"""
Define trusts when editing the trusting note
"""
model = Trust
fk_name = "trusting"
extra = 0
readonly_fields = ("trusted",)
@admin.register(Note, site=admin_site) @admin.register(Note, site=admin_site)
class NoteAdmin(PolymorphicParentModelAdmin): class NoteAdmin(PolymorphicParentModelAdmin):
""" """
@ -102,7 +92,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
""" """
Child for an user note, see NoteAdmin Child for an user note, see NoteAdmin
""" """
inlines = (AliasInlines, TrustInlines) inlines = (AliasInlines,)
# We can't change user after creation or the balance # We can't change user after creation or the balance
readonly_fields = ('user', 'balance') readonly_fields = ('user', 'balance')

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
@ -8,12 +8,11 @@ from rest_framework.exceptions import ValidationError
from rest_polymorphic.serializers import PolymorphicSerializer from rest_polymorphic.serializers import PolymorphicSerializer
from member.api.serializers import MembershipSerializer from member.api.serializers import MembershipSerializer
from member.models import Membership from member.models import Membership
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from rest_framework.utils import model_meta from rest_framework.utils import model_meta
from rest_framework.validators import UniqueTogetherValidator
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
RecurrentTransaction, SpecialTransaction RecurrentTransaction, SpecialTransaction
@ -78,20 +77,6 @@ class NoteUserSerializer(serializers.ModelSerializer):
return str(obj) return str(obj)
class TrustSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Trusts.
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
"""
class Meta:
model = Trust
fields = '__all__'
validators = [UniqueTogetherValidator(
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
message=_("This friendship already exists"))]
class AliasSerializer(serializers.ModelSerializer): class AliasSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Aliases. REST API Serializer for Aliases.
@ -141,7 +126,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
""" """
# If the user has no right to see the note, then we only display the note identifier # If the user has no right to see the note, then we only display the note identifier
return NotePolymorphicSerializer().to_representation(obj.note)\ return NotePolymorphicSerializer().to_representation(obj.note)\
if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\ if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
else dict( else dict(
id=obj.note.id, id=obj.note.id,
name=str(obj.note), name=str(obj.note),
@ -157,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
def get_membership(self, obj): def get_membership(self, obj):
if isinstance(obj.note, NoteUser): if isinstance(obj.note, NoteUser):
memberships = Membership.objects.filter( memberships = Membership.objects.filter(
PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter( PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
user=obj.note.user, user=obj.note.user,
club=2, # Kfet club=2, # Kfet
).order_by("-date_start") ).order_by("-date_start")

View File

@ -1,9 +1,8 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \ from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \ TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
TrustViewSet
def register_note_urls(router, path): def register_note_urls(router, path):
@ -12,7 +11,6 @@ def register_note_urls(router, path):
""" """
router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet) router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet) router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet)

View File

@ -1,6 +1,5 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import re
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.db.models import Q
@ -11,12 +10,12 @@ from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from note_kfet.middlewares import get_current_session
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \ from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \ TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
TrustSerializer from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@ -41,11 +40,12 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
Parse query and apply filters. Parse query and apply filters.
:return: The filtered set of requested notes :return: The filtered set of requested notes
""" """
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view") user = self.request.user
| PermissionBackend.filter_queryset(self.request, NoteUser, "view") get_current_session().setdefault("permission_mask", 42)
| PermissionBackend.filter_queryset(self.request, NoteClub, "view") queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\ | PermissionBackend.filter_queryset(user, NoteUser, "view")
.distinct() | PermissionBackend.filter_queryset(user, NoteClub, "view")
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
alias = self.request.query_params.get("alias", ".*") alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter( queryset = queryset.filter(
@ -57,55 +57,24 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
return queryset.order_by("id") return queryset.order_by("id")
class TrustViewSet(ReadProtectedModelViewSet):
"""
REST Trust View set.
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
then render it on /api/note/trust/
"""
queryset = Trust.objects
serializer_class = TrustSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
'$trusted__alias__name', '$trusted__alias__normalized_name']
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
ordering_fields = ['trusting', 'trusted', ]
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# trust relationship can't change people involved
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
class AliasViewSet(ReadProtectedModelViewSet): class AliasViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/note/aliases/ then render it on /api/aliases/
""" """
queryset = Alias.objects queryset = Alias.objects
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user', filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ] ordering_fields = ['name', 'normalized_name', ]
def get_serializer_class(self): def get_serializer_class(self):
serializer_class = self.serializer_class serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']: if self.request.method in ['PUT', 'PATCH']:
# alias owner cannot be change once establish # alias owner cannot be change once establish
serializer_class.Meta.read_only_fields = ('note',) setattr(serializer_class.Meta, 'read_only_fields', ('note',))
return serializer_class return serializer_class
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
@ -113,7 +82,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
try: try:
self.perform_destroy(instance) self.perform_destroy(instance)
except ValidationError as e: except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST) return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def get_queryset(self): def get_queryset(self):
@ -149,8 +118,7 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend] filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user', filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name', ] ordering_fields = ['name', 'normalized_name', ]
def get_queryset(self): def get_queryset(self):
@ -165,31 +133,23 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' else queryset
alias = self.request.query_params.get("alias", None) alias = self.request.query_params.get("alias", None)
# Check if this is a valid regex. If not, we won't check regex
try:
re.compile(alias)
valid_regex = True
except (re.error, TypeError):
valid_regex = False
suffix = '__iregex' if valid_regex else '__istartswith'
alias_prefix = '^' if valid_regex else ''
queryset = queryset.prefetch_related('note') queryset = queryset.prefetch_related('note')
if alias: if alias:
# We match first an alias if it is matched without normalization, # We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias. # then if the normalized pattern matches a normalized alias.
queryset = queryset.filter( queryset = queryset.filter(
**{f'name{suffix}': alias_prefix + alias} name__iregex="^" + alias
).union( ).union(
queryset.filter( queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(**{f'name{suffix}': alias_prefix + alias}) & ~Q(name__iregex="^" + alias)
), ),
all=True).union( all=True).union(
queryset.filter( queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()}) Q(normalized_name__iregex="^" + alias.lower())
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)}) & ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
& ~Q(**{f'name{suffix}': alias_prefix + alias}) & ~Q(name__iregex="^" + alias)
), ),
all=True) all=True)
@ -245,5 +205,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
ordering_fields = ['created_at', 'amount', ] ordering_fields = ['created_at', 'amount', ]
def get_queryset(self): def get_queryset(self):
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\ user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
.order_by("created_at", "id") .order_by("created_at", "id")

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime from datetime import datetime

View File

@ -1,19 +0,0 @@
# Generated by Django 2.2.19 on 2021-03-13 11:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0004_remove_null_tag_on_charfields'),
]
operations = [
migrations.AlterField(
model_name='alias',
name='note',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='alias', to='note.Note'),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 2.2.24 on 2021-09-05 19:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0005_auto_20210313_1235'),
]
operations = [
migrations.CreateModel(
name='Trust',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
],
options={
'verbose_name': 'frienship',
'verbose_name_plural': 'friendships',
'unique_together': {('trusting', 'trusted')},
},
),
]

View File

@ -1,13 +1,13 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \ from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [ __all__ = [
# Notes # Notes
'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions # Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction', 'SpecialTransaction', 'RecurrentTransaction', 'SpecialTransaction',

View File

@ -1,9 +1,10 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata import unicodedata
from django.conf import settings from django.conf import settings
from django.conf.global_settings import DEFAULT_FROM_EMAIL
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
@ -189,8 +190,8 @@ class NoteClub(Note):
def send_mail_negative_balance(self): def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=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)) html = render_to_string("note/mails/negative_balance.html", dict(note=self))
send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, send_mail("[Note Kfet] Passage en négatif (club {})".format(self.club.name), plain_text, DEFAULT_FROM_EMAIL,
settings.DEFAULT_FROM_EMAIL, [self.club.email], html_message=html) [self.club.email], html_message=html)
class NoteSpecial(Note): class NoteSpecial(Note):
@ -217,38 +218,6 @@ class NoteSpecial(Note):
return self.special_type return self.special_type
class Trust(models.Model):
"""
A one-sided trust relationship bertween two users
If another user considers you as your friend, you can transfer money from
them
"""
trusting = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='trusting',
verbose_name=_('trusting')
)
trusted = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='trusted',
verbose_name=_('trusted')
)
class Meta:
verbose_name = _("frienship")
verbose_name_plural = _("friendships")
unique_together = ("trusting", "trusted")
def __str__(self):
return _("Friendship between {trusting} and {trusted}").format(
trusting=str(self.trusting), trusted=str(self.trusted))
class Alias(models.Model): class Alias(models.Model):
""" """
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance. points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
@ -293,11 +262,6 @@ class Alias(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
@staticmethod @staticmethod
def normalize(string): def normalize(string):
""" """
@ -326,6 +290,11 @@ class Alias(models.Model):
pass pass
self.normalized_name = normalized_name self.normalized_name = normalized_name
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
if self.name == str(self.note): if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."), raise ValidationError(_("You can't delete your main alias."),

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -59,7 +59,6 @@ class TransactionTemplate(models.Model):
amount = models.PositiveIntegerField( amount = models.PositiveIntegerField(
verbose_name=_('amount'), verbose_name=_('amount'),
) )
category = models.ForeignKey( category = models.ForeignKey(
TemplateCategory, TemplateCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -88,12 +87,12 @@ class TransactionTemplate(models.Model):
verbose_name = _("transaction template") verbose_name = _("transaction template")
verbose_name_plural = _("transaction templates") verbose_name_plural = _("transaction templates")
def __str__(self):
return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('note:template_update', args=(self.pk,)) return reverse('note:template_update', args=(self.pk,))
def __str__(self):
return self.name
class Transaction(PolymorphicModel): class Transaction(PolymorphicModel):
""" """
@ -102,6 +101,7 @@ class Transaction(PolymorphicModel):
amount is store in centimes of currency, making it a positive integer amount is store in centimes of currency, making it a positive integer
value. (from someone to someone else) value. (from someone to someone else)
""" """
source = models.ForeignKey( source = models.ForeignKey(
Note, Note,
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -166,50 +166,6 @@ class Transaction(PolymorphicModel):
models.Index(fields=['destination']), models.Index(fields=['destination']),
] ]
def __str__(self):
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
@transaction.atomic
def save(self, *args, **kwargs):
"""
When saving, also transfer money between two notes
"""
if self.source.pk == self.destination.pk:
# When source == destination, no money is transferred and no transaction is created
return
self.source = Note.objects.select_for_update().get(pk=self.source_id)
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
# Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate()
if not (hasattr(self, '_force_save') and self._force_save) \
and (not self.source.is_active or not self.destination.is_active):
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
# If the aliases are not entered, we assume that the used alias is the name of the note
if not self.source_alias:
self.source_alias = str(self.source)
if not self.destination_alias:
self.destination_alias = str(self.destination)
# We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs)
# Save notes
self.source.refresh_from_db()
self.source.balance += diff_source
self.source._force_save = True
self.source.save()
self.destination.refresh_from_db()
self.destination.balance += diff_dest
self.destination._force_save = True
self.destination.save()
def validate(self): def validate(self):
previous_source_balance = self.source.balance previous_source_balance = self.source.balance
previous_dest_balance = self.destination.balance previous_dest_balance = self.destination.balance
@ -252,6 +208,46 @@ class Transaction(PolymorphicModel):
return source_balance - previous_source_balance, dest_balance - previous_dest_balance return source_balance - previous_source_balance, dest_balance - previous_dest_balance
@transaction.atomic
def save(self, *args, **kwargs):
"""
When saving, also transfer money between two notes
"""
if self.source.pk == self.destination.pk:
# When source == destination, no money is transferred and no transaction is created
return
self.source = Note.objects.select_for_update().get(pk=self.source_id)
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
# Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate()
if not (hasattr(self, '_force_save') and self._force_save) \
and (not self.source.is_active or not self.destination.is_active):
raise ValidationError(_("The transaction can't be saved since the source note "
"or the destination note is not active."))
# If the aliases are not entered, we assume that the used alias is the name of the note
if not self.source_alias:
self.source_alias = str(self.source)
if not self.destination_alias:
self.destination_alias = str(self.destination)
# We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs)
# Save notes
self.source.refresh_from_db()
self.source.balance += diff_source
self.source._force_save = True
self.source.save()
self.destination.refresh_from_db()
self.destination.balance += diff_dest
self.destination._force_save = True
self.destination.save()
@property @property
def total(self): def total(self):
return self.amount * self.quantity return self.amount * self.quantity
@ -260,40 +256,46 @@ class Transaction(PolymorphicModel):
def type(self): def type(self):
return _('Transfer') return _('Transfer')
def __str__(self):
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
class RecurrentTransaction(Transaction): class RecurrentTransaction(Transaction):
""" """
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
""" """
template = models.ForeignKey( template = models.ForeignKey(
TransactionTemplate, TransactionTemplate,
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
class Meta:
verbose_name = _("recurrent transaction")
verbose_name_plural = _("recurrent transactions")
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
return super().save(*args, **kwargs)
def clean(self): def clean(self):
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save): if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
raise ValidationError( raise ValidationError(
_("The destination of this transaction must equal to the destination of the template.")) _("The destination of this transaction must equal to the destination of the template."))
return super().clean() return super().clean()
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
return super().save(*args, **kwargs)
@property @property
def type(self): def type(self):
return _('Template') return _('Template')
class Meta:
verbose_name = _("recurrent transaction")
verbose_name_plural = _("recurrent transactions")
class SpecialTransaction(Transaction): class SpecialTransaction(Transaction):
""" """
Special type of :model:`note.Transaction` associated to transactions with special notes Special type of :model:`note.Transaction` associated to transactions with special notes
""" """
last_name = models.CharField( last_name = models.CharField(
max_length=255, max_length=255,
verbose_name=_("name"), verbose_name=_("name"),
@ -310,15 +312,6 @@ class SpecialTransaction(Transaction):
blank=True, blank=True,
) )
class Meta:
verbose_name = _("Special transaction")
verbose_name_plural = _("Special transactions")
@transaction.atomic
def save(self, *args, **kwargs):
self.clean()
super().save(*args, **kwargs)
@property @property
def type(self): def type(self):
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit") return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
@ -332,44 +325,25 @@ class SpecialTransaction(Transaction):
def clean(self): def clean(self):
# SpecialTransaction are only possible with NoteSpecial object # SpecialTransaction are only possible with NoteSpecial object
if self.is_credit() == self.is_debit(): if self.is_credit() == self.is_debit():
raise ValidationError(_("A special transaction is only possible between a" raise(ValidationError(_("A special transaction is only possible between a"
" Note associated to a payment method and a User or a Club")) " Note associated to a payment method and a User or a Club")))
@staticmethod @transaction.atomic
def validate_payment_form(form): def save(self, *args, **kwargs):
""" self.clean()
Ensure that last name and first name are filled for a form that creates a SpecialTransaction, super().save(*args, **kwargs)
and check that if the user pays with a check, then the bank field is filled.
Return True iff there is no error. class Meta:
Whenever there is an error, they are inserted in the form errors. verbose_name = _("Special transaction")
""" verbose_name_plural = _("Special transactions")
credit_type = form.cleaned_data["credit_type"]
last_name = form.cleaned_data["last_name"]
first_name = form.cleaned_data["first_name"]
bank = form.cleaned_data["bank"]
error = False
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."))
error = True
return not error
class MembershipTransaction(Transaction): class MembershipTransaction(Transaction):
""" """
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`. Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
""" """
membership = models.OneToOneField( membership = models.OneToOneField(
'member.Membership', 'member.Membership',
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone from django.utils import timezone

View File

@ -1,4 +1,4 @@
// Copyright (C) 2018-2024 by BDE ENS Paris-Saclay // Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks. // When a transaction is performed, lock the interface to prevent spam clicks.
@ -28,7 +28,7 @@ $(document).ready(function () {
// Switching in double consumptions mode should update the layout // Switching in double consumptions mode should update the layout
$('#double_conso').change(function () { $('#double_conso').change(function () {
document.getElementById('consos_list_div').classList.remove('d-none') $('#consos_list_div').removeClass('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-xl-6') $('#infos_div').attr('class', 'col-sm-5 col-xl-6')
const note_list_obj = $('#note_list') const note_list_obj = $('#note_list')
@ -37,7 +37,7 @@ $(document).ready(function () {
note_list_obj.html('') note_list_obj.html('')
buttons.forEach(function (button) { buttons.forEach(function (button) {
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => { $('#conso_button_' + button.id).click(function () {
if (LOCK) { return } if (LOCK) { return }
removeNote(button, 'conso_button', buttons, 'consos_list')() removeNote(button, 'conso_button', buttons, 'consos_list')()
}) })
@ -46,7 +46,7 @@ $(document).ready(function () {
}) })
$('#single_conso').change(function () { $('#single_conso').change(function () {
document.getElementById('consos_list_div').classList.add('d-none') $('#consos_list_div').addClass('d-none')
$('#infos_div').attr('class', 'col-sm-5 col-md-4') $('#infos_div').attr('class', 'col-sm-5 col-md-4')
const consos_list_obj = $('#consos_list') const consos_list_obj = $('#consos_list')
@ -68,9 +68,9 @@ $(document).ready(function () {
}) })
// Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS
document.querySelector("label[for='double_conso']").classList.remove('active') $("label[for='double_conso']").removeClass('active')
document.getElementById("consume_all").addEventListener('click', consumeAll) $('#consume_all').click(consumeAll)
}) })
notes = [] notes = []
@ -127,10 +127,11 @@ function addConso (dest, amount, type, category_id, category_name, template_id,
html += li('conso_button_' + button.id, button.name + html += li('conso_button_' + button.id, button.name +
'<span class="badge badge-dark badge-pill">' + button.quantity + '</span>') '<span class="badge badge-dark badge-pill">' + button.quantity + '</span>')
}) })
document.getElementById(list).innerHTML = html
buttons.forEach((button) => { $('#' + list).html(html)
document.getElementById(`conso_button_${button.id}`).addEventListener('click', () => {
buttons.forEach(function (button) {
$('#conso_button_' + button.id).click(function () {
if (LOCK) { return } if (LOCK) { return }
removeNote(button, 'conso_button', buttons, list)() removeNote(button, 'conso_button', buttons, list)()
}) })
@ -145,13 +146,12 @@ function reset () {
notes_display.length = 0 notes_display.length = 0
notes.length = 0 notes.length = 0
buttons.length = 0 buttons.length = 0
document.getElementById('note_list').innerHTML = '' $('#note_list').html('')
document.getElementById('consos_list').innerHTML = '' $('#consos_list').html('')
document.getElementById('note').value = '' $('#note').val('')
document.getElementById('note').dataset.originTitle = '' $('#note').attr('data-original-title', '').tooltip('hide')
$('#note').tooltip('hide') $('#profile_pic').attr('src', '/static/member/img/default_picture.png')
document.getElementById('profile_pic').src = '/static/member/img/default_picture.png' $('#profile_pic_link').attr('href', '#')
document.getElementById('profile_pic_link').href = '#'
refreshHistory() refreshHistory()
refreshBalance() refreshBalance()
LOCK = false LOCK = false
@ -168,7 +168,7 @@ function consumeAll () {
let error = false let error = false
if (notes_display.length === 0) { if (notes_display.length === 0) {
document.getElementById('note').classList.add('is-invalid') $('#note').addClass('is-invalid')
$('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger')) $('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger'))
error = true error = true
} }
@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
.done(function () { .done(function () {
if (!isNaN(source.balance)) { if (!isNaN(source.balance)) {
const newBalance = source.balance - quantity * amount const newBalance = source.balance - quantity * amount
if (newBalance <= -2000) { if (newBalance <= -5000) {
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + 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) 'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
} else if (newBalance < 0) { } else if (newBalance < 0) {
@ -258,39 +258,3 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
}) })
}) })
} }
var searchbar = document.getElementById("search-input")
var search_results = document.getElementById("search-results")
var old_pattern = null;
var firstMatch = null;
/**
* Updates the button search tab
* @param force Forces the update even if the pattern didn't change
*/
function updateSearch(force = false) {
let pattern = searchbar.value
if (pattern === "")
firstMatch = null;
if ((pattern === old_pattern || pattern === "") && !force)
return;
firstMatch = null;
const re = new RegExp(pattern, "i");
Array.from(search_results.children).forEach(function(b) {
if (re.test(b.innerText)) {
b.hidden = false;
if (firstMatch === null) {
firstMatch = b;
}
} else
b.hidden = true;
});
}
searchbar.addEventListener("input", function (e) {
debounce(updateSearch)()
});
searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter")
firstMatch.click()
});

View File

@ -222,13 +222,6 @@ $(document).ready(function () {
}) })
}) })
// Make transfer when pressing Enter on the amount section
$('#amount, #reason, #last_name, #first_name, #bank').keypress((event) => {
if (event.originalEvent.charCode === 13) {
$('#btn_transfer').click()
}
})
$('#btn_transfer').click(function () { $('#btn_transfer').click(function () {
if (LOCK) { return } if (LOCK) { return }
@ -250,7 +243,7 @@ $('#btn_transfer').click(function () {
error = true error = true
} }
const amount = Math.round(100 * amount_field.val()) const amount = Math.floor(100 * amount_field.val())
if (amount > 2147483647) { if (amount > 2147483647) {
amount_field.addClass('is-invalid') amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>') $('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
@ -314,7 +307,7 @@ $('#btn_transfer').click(function () {
if (!isNaN(source.note.balance)) { if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -2000) { if (newBalance <= -5000) {
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.'), 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) [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset() reset()
@ -355,14 +348,14 @@ $('#btn_transfer').click(function () {
destination_alias: dest.name destination_alias: dest.name
}).done(function () { }).done(function () {
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), 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) [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000)
reset() reset()
}).fail(function (err) { }).fail(function (err) {
const errObj = JSON.parse(err.responseText) const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText } if (!error) { error = err.responseText }
addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'), 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') [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
LOCK = false LOCK = false
}) })
}) })

View File

@ -1,16 +1,16 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import html import html
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html, mark_safe from django.utils.html import format_html
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from .models.notes import Alias, Trust from .models.notes import Alias
from .models.transactions import Transaction, TransactionTemplate from .models.transactions import Transaction, TransactionTemplate
from .templatetags.pretty_money import pretty_money from .templatetags.pretty_money import pretty_money
@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
"class": lambda record: "class": lambda record:
str(record.valid).lower() str(record.valid).lower()
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend + (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record) .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
else ''), else ''),
"data-toggle": "tooltip", "data-toggle": "tooltip",
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate")) "title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
if PermissionBackend.check_perm(get_current_request(), if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) "note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None, and record.source.is_active and record.destination.is_active else None,
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
+ ', "' + str(record.__class__.__name__) + '")' + ', "' + str(record.__class__.__name__) + '")'
if PermissionBackend.check_perm(get_current_request(), if PermissionBackend.check_perm(get_current_authenticated_user(),
"note.change_transaction_invalidity_reason", record) "note.change_transaction_invalidity_reason", record)
and record.source.is_active and record.destination.is_active else None, and record.source.is_active and record.destination.is_active else None,
"onmouseover": lambda record: '$("#invalidity_reason_' "onmouseover": lambda record: '$("#invalidity_reason_'
@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
""" """
has_perm = PermissionBackend \ has_perm = PermissionBackend \
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record) .check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
val = "" if value else "" val = "" if value else ""
@ -148,71 +148,6 @@ DELETE_TEMPLATE = """
""" """
class TrustTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': "trust_table"
}
model = Trust
fields = ("trusted",)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
trusted = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('Delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
+ (' d-none' if not PermissionBackend.check_perm(
get_current_request(), "note.delete_trust", record)
else '')}},
verbose_name=_("Delete"),)
class TrustedTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': 'trusted_table'
}
Model = Trust
fields = ("trusting",)
template_name = "django_tables2/bootstrap4.html"
show_header = False
trusting = tables.Column(attrs={
'td': {'class': 'text-center', 'width': '100%'}})
trust_back = tables.Column(
verbose_name=_("Trust back"),
accessor="pk",
attrs={
'td': {
'class': '',
'id': lambda record: "trust_back_" + str(record.pk),
}
},
)
def render_trust_back(self, record):
user_note = record.trusted
trusting_note = record.trusting
if Trust.objects.filter(trusted=trusting_note, trusting=user_note):
return ""
val = '<button id="'
val += str(record.pk)
val += '" class="btn btn-success btn-sm text-nowrap" \
onclick="create_trust(' + str(record.trusted.pk) + ',' + \
str(record.trusting.pk) + ')">'
val += str(_("Add back"))
val += '</button>'
return mark_safe(val)
class AliasTable(tables.Table): class AliasTable(tables.Table):
class Meta: class Meta:
attrs = { attrs = {
@ -230,7 +165,7 @@ class AliasTable(tables.Table):
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': lambda record: 'col-sm-1' + ( attrs={'td': {'class': lambda record: 'col-sm-1' + (
' d-none' if not PermissionBackend.check_perm( ' d-none' if not PermissionBackend.check_perm(
get_current_request(), "note.delete_alias", get_current_authenticated_user(), "note.delete_alias",
record) else '')}}, verbose_name=_("Delete"), ) record) else '')}}, verbose_name=_("Delete"), )
@ -262,17 +197,6 @@ class ButtonTable(tables.Table):
verbose_name=_("Edit"), verbose_name=_("Edit"),
) )
hideshow = tables.Column(
verbose_name=_("Hide/Show"),
accessor="pk",
attrs={
'td': {
'class': 'col-sm-1',
'id': lambda record: "hideshow_" + str(record.pk),
}
},
)
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}}, attrs={'td': {'class': 'col-sm-1'}},
@ -280,16 +204,3 @@ class ButtonTable(tables.Table):
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)
def order_category(self, queryset, is_descending):
return queryset.order_by(f"{'-' if is_descending else ''}category__name"), True
def render_hideshow(self, record):
val = '<button id="'
val += str(record.pk)
val += '" class="btn btn-secondary btn-sm" \
onclick="hideshow(' + str(record.id) + ',' + \
str(record.display).lower() + ')">'
val += str(_("Hide/Show"))
val += '</button>'
return mark_safe(val)

View File

@ -103,11 +103,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
{% trans "Search" %}
</a>
</li>
</ul> </ul>
</div> </div>
@ -128,20 +123,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="tab-pane" id="search">
<input class="form-control mx-auto d-block mb-3"
placeholder="{% trans "Search button..." %}" type="search" id="search-input"/>
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
{% for button in all_buttons %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
id="search_button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
@ -182,7 +163,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<script type="text/javascript"> <script type="text/javascript">
{% for button in highlighted %} {% for button in highlighted %}
{% if button.display %} {% if button.display %}
document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() { $("#highlighted_button{{ button.id }}").click(function() {
addConso({{ button.destination_id }}, {{ button.amount }}, addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}"); {{ button.id }}, "{{ button.name|escapejs }}");
@ -193,7 +174,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% for category in categories %} {% for category in categories %}
{% for button in category.templates_filtered %} {% for button in category.templates_filtered %}
{% if button.display %} {% if button.display %}
document.getElementById("button{{ button.id }}").addEventListener("click", function() { $("#button{{ button.id }}").click(function() {
addConso({{ button.destination_id }}, {{ button.amount }}, addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}", {{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}"); {{ button.id }}, "{{ button.name|escapejs }}");
@ -201,15 +182,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% for button in all_buttons %}
{% if button.display %}
document.getElementById("search_button{{ button.id }}").addEventListener("click", function() {
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
});
{% endif %}
{% endfor %}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -23,7 +23,7 @@
<p> <p>
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
est inférieur à 0 €. est inférieur à 0 € depuis plus de 24h.
</p> </p>
<p> <p>
@ -43,4 +43,4 @@
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %} {% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
</p> </p>
</body> </body>
</html> </html>

View File

@ -9,7 +9,7 @@ Ce mail t'a été envoyé parce que le solde de ta Note Kfet
Ton solde actuel est de {{ note.balance|pretty_money }}. Ton solde actuel est de {{ note.balance|pretty_money }}.
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent·e·s dont le solde Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
est inférieur à 0 € depuis plus de 24h. est inférieur à 0 € depuis plus de 24h.
Si tu ne comprends pas ton solde, tu peux consulter ton historique Si tu ne comprends pas ton solde, tu peux consulter ton historique

View File

@ -10,25 +10,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
{# bandeau transfert/crédit/débit/activité #} {# bandeau transfert/crédit/débit/activité #}
<div class="row"> <div class="row">
<div class="col-xl-12"> <div class="col-xl-12">
<div class="btn-group btn-block"> <div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons"> <label for="type_transfer" class="btn btn-sm btn-outline-primary active">
<label for="type_transfer" class="btn btn-sm btn-outline-primary active"> <input type="radio" name="transaction_type" id="type_transfer">
<input type="radio" name="transaction_type" id="type_transfer"> {% trans "Transfer" %}
{% trans "Transfer" %} </label>
{% if "note.notespecial"|not_empty_model_list %}
<label for="type_credit" class="btn btn-sm btn-outline-primary">
<input type="radio" name="transaction_type" id="type_credit">
{% trans "Credit" %}
</label> </label>
{% if "note.notespecial"|not_empty_model_list %} <label for="type_debit" class="btn btn-sm btn-outline-primary">
<label for="type_credit" class="btn btn-sm btn-outline-primary"> <input type="radio" name="transaction_type" id="type_debit">
<input type="radio" name="transaction_type" id="type_credit"> {% trans "Debit" %}
{% trans "Credit" %} </label>
</label> {% endif %}
<label for="type_debit" class="btn btn-sm btn-outline-primary">
<input type="radio" name="transaction_type" id="type_debit">
{% trans "Debit" %}
</label>
{% endif %}
</div>
{# Add shortcuts for opened activites if necessary #}
{% for activity in activities_open %} {% for activity in activities_open %}
<a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary"> <a href="{% url "activity:activity_entry" pk=activity.pk %}" class="btn btn-sm btn-outline-primary">
{% trans "Entries" %} {{ activity.name }} {% trans "Entries" %} {{ activity.name }}
@ -61,7 +57,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ul class="list-group list-group-flush" id="source_note_list"> <ul class="list-group list-group-flush" id="source_note_list">
</ul> </ul>
<div class="card-body"> <div class="card-body">
<select id="credit_type" class="form-control custom-select d-none"> <select id="credit_type" class="custom-select d-none">
{% for special_type in special_types %} {% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option> <option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %} {% endfor %}
@ -88,7 +84,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ul class="list-group list-group-flush" id="dest_note_list"> <ul class="list-group list-group-flush" id="dest_note_list">
</ul> </ul>
<div class="card-body"> <div class="card-body">
<select id="debit_type" class="form-control custom-select d-none"> <select id="debit_type" class="custom-select d-none">
{% for special_type in special_types %} {% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option> <option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %} {% endfor %}

View File

@ -31,29 +31,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript"> <script type="text/javascript">
function refreshMatchedWords() {
$("tr").each(function() {
let pattern = $('#search_field').val();
if (pattern) {
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
});
}
});
}
function reloadTable() {
let pattern = $('#search_field').val();
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
}
$(document).ready(function() { $(document).ready(function() {
let searchbar_obj = $("#search_field"); let searchbar_obj = $("#search_field");
let timer_on = false; let timer_on = false;
let timer; let timer;
function refreshMatchedWords() {
$("tr").each(function() {
let pattern = searchbar_obj.val();
if (pattern) {
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
});
}
});
}
refreshMatchedWords(); refreshMatchedWords();
function reloadTable() {
let pattern = searchbar_obj.val();
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
}
searchbar_obj.keyup(function() { searchbar_obj.keyup(function() {
if (timer_on) if (timer_on)
clearTimeout(timer); clearTimeout(timer);
@ -77,28 +77,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger') addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
}); });
} }
// on click of button "hide/show", call the API
function hideshow(id, displayed) {
$.ajax({
url: '/api/note/transaction/template/' + id + '/',
type: 'PATCH',
dataType: 'json',
headers: {
'X-CSRFTOKEN': CSRF_TOKEN
},
data: {
display: !displayed
},
success: function() {
if(displayed)
addMsg("{% trans "Button hidden"%}", 'success', 1000)
else addMsg("{% trans "Button displayed"%}", 'success', 1000)
reloadTable()
},
error: function (err) {
addMsg("{% trans "An error occured"%}", 'danger')
}})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI from api.tests import TestAPI
@ -10,7 +10,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from permission.models import Role from permission.models import Role
from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet, \ from ..api.views import AliasViewSet, ConsumerViewSet, NotePolymorphicViewSet, TemplateCategoryViewSet,\
TransactionTemplateViewSet, TransactionViewSet TransactionTemplateViewSet, TransactionViewSet
from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \ from ..models import NoteUser, Transaction, TemplateCategory, TransactionTemplate, RecurrentTransaction, \
MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note MembershipTransaction, SpecialTransaction, NoteSpecial, Alias, Note

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

Some files were not shown because too many files have changed in this diff Show More