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

Compare commits

..

100 Commits

Author SHA1 Message Date
1c5e951c2f Merge branch 'beta' into 'master'
Various fixes

See merge request bde/nk20!133
2020-10-07 12:06:48 +02:00
beb1853aef Forgot to create the aliases for BDE and Kfet in the migration that create the clubs 2020-10-07 11:54:04 +02:00
0078eb8f90 Index page is a redirection 2020-10-07 11:53:42 +02:00
e5e758f9d9 Display banners when a user is no more a BDE or Kfet member 2020-10-07 11:46:43 +02:00
4a78328717 The checkbox to tell that a Sogé account got opened is not mandatory 2020-10-07 11:31:20 +02:00
65a2e8c08c Better index page: non-Kfet members will be redirected to their profile page, the account note (when it will be managed) will see the consumption page 2020-10-07 11:29:52 +02:00
b5fa428bad Non-Kfet members can see their old aliases only, but no one else 2020-10-07 11:22:02 +02:00
fb72385773 Warn users that they have to open they Sogé account 2020-10-07 10:59:37 +02:00
2f68601e8b Delete the soge credit if the user declares that one was opened but in the validation form the checkbox was unchecked 2020-10-07 10:46:33 +02:00
0b1bed8048 Temporary give the right to treasurers to manage membership roles, but need to find a proper solution 2020-10-07 10:43:58 +02:00
8ada0e51f2 The validation filter of the soge credit list was buggy 2020-10-07 10:42:52 +02:00
c3d613947f Pre-registered users can declare that they opened a bank account in the signup form 2020-10-07 10:33:57 +02:00
36b8157372 Fix membership table order 2020-10-07 10:03:43 +02:00
992cfe8e23 Can set a parent club to None 2020-10-07 09:48:21 +02:00
18a8ff1b8a Set credit/debit reason non mandatory 2020-10-07 09:45:09 +02:00
c61bb2e90d When we credit the note of a club directly, fill the last name and the first name information with the club name instead of empty 2020-10-07 09:39:40 +02:00
4b12e3ed08 Display only the most recent membership 2020-10-07 09:29:41 +02:00
af07ed9807 Merge branch 'erdnaxe-master-patch-99095' into 'beta'
Erdnaxe master patch 99095

See merge request bde/nk20!132
2020-10-05 16:37:14 +02:00
bbe53b3b63 Update README.md 2020-10-05 16:25:06 +02:00
536f0ec226 Merge branch 'beta' into 'master'
Ensure the integrity of all note balances in multiple transactions

See merge request bde/nk20!131
2020-10-04 22:12:12 +02:00
541ed59f40 When a membership is created, redirect to the user profile page rather than club detail 2020-10-04 21:08:35 +02:00
e172b4f4bb When a membership is renewed, set the same roles as the previous membership 2020-10-04 20:54:03 +02:00
d666179037 Display Renew membership button 15 days more 2020-10-04 20:50:10 +02:00
f22e92132c Select for update transaction notes, and not only the transaction 2020-10-04 20:47:15 +02:00
ca7ad05746 Use a signal to prevent a user that the note balance is negative 2020-10-04 20:26:43 +02:00
f55ca2f725 Merge branch 'beta' into 'master'
remove the display limit for pre-registred users.

See merge request bde/nk20!130
2020-10-03 18:07:39 +02:00
d4e4ed580f remove the display limit for pre-registred users. 2020-10-03 17:53:38 +02:00
8756751344 Merge branch 'beta' into 'master'
Fix some membership issues

See merge request bde/nk20!129
2020-10-01 15:00:36 +02:00
fd83fe19bf Fix some membership date control 2020-10-01 09:17:02 +02:00
a00d95608b Add permission to treasurers to create a club, fix the permission check to renew a membership 2020-09-23 21:36:04 +02:00
3303edd01f Merge branch 'beta' into 'master'
OAuth2, no side effect permissions

See merge request bde/nk20!128
2020-09-23 18:44:40 +02:00
e48ef92137 Revert commit that broke beta branch 2020-09-23 18:32:09 +02:00
919d0b7e85 Merge branch 'ics_cache' into 'beta'
Ics cache

See merge request bde/nk20!127
2020-09-21 15:46:34 +02:00
439bf35b62 APT python memcache is PyPi memcached 2020-09-21 15:25:07 +02:00
74b26335d1 Cache ICS calendar 2020-09-21 15:13:59 +02:00
3d733ed6af Use memcached cache 2020-09-21 15:13:43 +02:00
d54ab94ceb Merge branch 'oauth' into 'beta'
Oauth

See merge request bde/nk20!126
2020-09-21 12:53:03 +02:00
4f188ca3e5 Admin is autodiscovering partially 2020-09-21 12:34:34 +02:00
72bac75fbd Add Django OAuth toolkit admin 2020-09-21 12:15:40 +02:00
6d54aae614 Fix django-oauth-toolkit version 2020-09-21 11:15:00 +02:00
8052152ea5 Add OAuth2 endpoints 2020-09-21 11:03:07 +02:00
70448db8e5 Remove Django CAS server and add oauth toolkit 2020-09-21 10:31:42 +02:00
ac2d1e8111 Merge branch 'no_side_effect_permission_check' into 'beta'
No side effect permission check

See merge request bde/nk20!124
2020-09-20 11:24:46 +02:00
3ba61385a3 Debit is not credit 2020-09-20 11:12:44 +02:00
7353348d7a Rollback transaction when checking an add permission (experimental) 2020-09-20 09:07:51 +02:00
f63e2e088e Don't log when the permission to lock a note is checked 2020-09-20 08:56:42 +02:00
d566def706 Try to translate js, not working... 2020-09-19 22:03:45 +02:00
eaf6769e8b Treasurers can make transactions with people that are no longer a member 2020-09-19 16:33:52 +02:00
a61ec81cff note.crans.org is the default domain 2020-09-19 16:03:32 +02:00
60f2a73cc5 Don't check if the user is a member of the parent club if there is no parent club 2020-09-18 13:35:55 +02:00
bcd96b2ed8 The BDE membership and the club membership must now be in two parts 2020-09-18 12:35:36 +02:00
5c702187e5 Merge branch 'beta' into 'master'
Corrections diverses

See merge request bde/nk20!123
2020-09-14 10:16:13 +02:00
905d65371f The user validation form was ugly 2020-09-14 09:56:15 +02:00
180cd3e1ec Fix registration permissions and procedure 2020-09-14 09:49:30 +02:00
73ca65aa91 Merge branch 'atomicity' into 'beta'
Atomicité

See merge request bde/nk20!122
2020-09-14 09:38:54 +02:00
5ed0560953 Fix linting 2020-09-14 09:09:20 +02:00
dbc6fbbf71 Fix the validation clicker issue, now the note is safe 2020-09-14 09:05:35 +02:00
872fd8f86d Don't cache permissions in debug mode, that's very slow 2020-09-14 08:58:12 +02:00
f89234b69a JS was broken, please close your HTML tags 2020-09-13 23:18:00 +02:00
36a980555b Revert "Make the nk20 usable for pirates"
This reverts commit 0f53ac45f7.
2020-09-13 20:42:44 +02:00
826cd4d87f Revert "Use underscore in locales"
This reverts commit 2270a0aa82.
2020-09-13 20:42:34 +02:00
e8005a6c58 Update Django locale selector 2020-09-13 20:17:59 +02:00
2270a0aa82 Use underscore in locales 2020-09-13 20:10:26 +02:00
0f53ac45f7 Make the nk20 usable for pirates 2020-09-13 20:05:06 +02:00
670556c59e Merge branch 'traduction' into 'beta'
End of Spanish translation

See merge request bde/nk20!121
2020-09-13 18:55:56 +02:00
5b02ba48e0 Some OTL, but so much remain 2020-09-13 12:40:10 +02:00
f3f18bc25e Merge branch 'beta' into traduction 2020-09-13 12:17:25 +02:00
03124e124c Translation : typo 2020-09-13 11:52:43 +02:00
6308964e93 Translation : French OTL 2020-09-13 11:52:13 +02:00
ed79097288 Spanish translation : ~end 2020-09-13 11:51:29 +02:00
d7eaef8cee Spanish translation : 88%, ¡ quedan 77 para mañana ! 2020-09-12 22:54:41 +02:00
01d405e54b Spanish translation : 70%, a comer! 2020-09-12 20:18:11 +02:00
80e3cba4c6 BDE Treasurers can see the remittance interface 2020-09-12 18:40:14 +02:00
f190053e84 Display the right amount in soge credit detail 2020-09-12 18:36:05 +02:00
218960adb5 Spanish translation : 57%, not always 57 2020-09-12 16:34:24 +02:00
88a1eae631 Spanish translation : 42%, always 42 2020-09-12 11:59:59 +02:00
2a2ecb2acc Activate es locale 2020-09-12 09:17:15 +02:00
f5486bdb63 Merge branch 'traduction' into 'beta'
Traduction

See merge request bde/nk20!120
2020-09-12 09:19:04 +02:00
9b090a145c All transactions are now atomic 2020-09-11 22:52:16 +02:00
860c7b50e5 Filter a consumer by its note id 2020-09-10 14:42:52 +02:00
afdc75c0bd Access to consumer object wa buggy 2020-09-10 14:41:09 +02:00
c6603e8aa7 Add more filters in the API 2020-09-10 14:37:11 +02:00
72cc1638e6 Authenticate correctly users that connect with an authorization token 2020-09-10 09:31:27 +02:00
6a0dc4cb10 Users can see every API page since querysets are filtered and modifications are protected 2020-09-09 22:27:07 +02:00
0f1f3b9560 Do not serve static files outside of debug server 2020-09-09 17:14:03 +02:00
c720e5483e Move transfer.js where it belongs 2020-09-09 16:45:15 +02:00
0fd3e9db78 Move consos.js where it belongs 2020-09-09 16:42:45 +02:00
c34296c923 Merge branch 'fixed_width_image' into 'beta'
Fix profile picture width

See merge request bde/nk20!119
2020-09-09 15:18:50 +02:00
ce4c22a4a1 Smaller text and larger padding on note label 2020-09-09 15:03:34 +02:00
3e0f665ef8 Resync es translation 2020-09-09 14:32:01 +02:00
be8751c815 Merge branch 'beta' into traduction 2020-09-09 14:28:19 +02:00
8225445c3e Update translations 2020-09-09 14:10:07 +02:00
f333e6a875 Fix profile picture width 2020-09-09 14:03:49 +02:00
e5835b46a5 Backups are sent to Zamok 2020-09-08 13:31:22 +02:00
fe937405a6 Merge remote-tracking branch 'origin/beta' into beta 2020-09-08 10:11:44 +02:00
0741c8ad2b Refactor the script to extract the mails that are registered to an events mailing list 2020-09-08 10:11:33 +02:00
3191dba31f Merge branch 'beta' into 'master'
Fix permissions to let treasurers to make some initial registrations

See merge request bde/nk20!118
2020-09-07 23:52:12 +02:00
428de69d93 Fix permissions to let treasurers to make some initial registrations 2020-09-07 23:36:50 +02:00
0888afe439 I am hungry, so I ham hungry 2020-09-04 20:38:57 +02:00
3111c30e56 Add spanish translation 2020-09-04 18:24:49 +02:00
63 changed files with 4594 additions and 936 deletions

View File

@ -16,8 +16,8 @@ py37-django22:
apt-get install --no-install-recommends -t buster-backports -y apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-crispy-forms python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py37-django22 script: tox -e py37-django22
@ -33,8 +33,8 @@ py38-django22:
apt-get install --no-install-recommends -y apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py38-django22 script: tox -e py38-django22

View File

@ -8,8 +8,8 @@ RUN apt-get update && \
apt-get install --no-install-recommends -t buster-backports -y \ apt-get install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \ python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \ python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \ uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \

View File

@ -93,10 +93,10 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ sudo apt install --no-install-recommends -t buster-backports -y \ $ sudo apt install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \ python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \ python3-django-extensions python3-django-filters python3-django-polymorphic \
python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools python3-docutils \
uwsgi uwsgi-plugin-python3 \ memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \ texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
nginx python3-venv git acl nginx python3-venv git acl
``` ```

View File

@ -23,13 +23,14 @@
- python3-babel - python3-babel
- python3-bs4 - python3-bs4
- python3-django - python3-django
- python3-django-cas-server
- python3-django-crispy-forms - python3-django-crispy-forms
- python3-django-extensions - python3-django-extensions
- python3-django-filters - python3-django-filters
- python3-django-oauth-toolkit
- python3-django-polymorphic - python3-django-polymorphic
- python3-djangorestframework - python3-djangorestframework
- python3-lockfile - python3-lockfile
- python3-memcache
- python3-phonenumbers - python3-phonenumbers
- python3-pil - python3-pil
- python3-pip - python3-pip
@ -40,6 +41,9 @@
# LaTeX (PDF generation) # LaTeX (PDF generation)
- texlive-xetex - texlive-xetex
# Cache server
- memcached
# WSGI server # WSGI server
- uwsgi - uwsgi
- uwsgi-plugin-python3 - uwsgi-plugin-python3

View File

@ -7,7 +7,7 @@ from threading import Thread
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -123,6 +123,7 @@ class Activity(models.Model):
verbose_name=_('open'), verbose_name=_('open'),
) )
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
Update the activity wiki page each time the activity is updated (validation, change description, ...) Update the activity wiki page each time the activity is updated (validation, change description, ...)
@ -194,8 +195,8 @@ class Entry(models.Model):
else _("Entry for {note} to the activity {activity}").format( else _("Entry for {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity)) guest=str(self.guest), note=str(self.note), activity=str(self.activity))
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
@ -260,6 +261,7 @@ class Guest(models.Model):
except AttributeError: except AttributeError:
return False return False
@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):
one_year = timedelta(days=365) one_year = timedelta(days=365)

View File

@ -7,12 +7,15 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import F, Q from django.db.models import F, Q
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ 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.generic import DetailView, TemplateView, UpdateView from django.views.generic import DetailView, TemplateView, UpdateView
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser from note.models import Alias, NoteSpecial, NoteUser
@ -44,6 +47,7 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_end=timezone.now(), date_end=timezone.now(),
) )
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.creater = self.request.user form.instance.creater = self.request.user
return super().form_valid(form) return super().form_valid(form)
@ -145,6 +149,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
form.fields["inviter"].initial = self.request.user.note form.fields["inviter"].initial = self.request.user.note
return form return form
@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.user, Activity, "view")).get(pk=self.kwargs["pk"]) .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
@ -285,6 +290,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
return context return context
# Cache for 1 hour
@method_decorator(cache_page(60 * 60), name='dispatch')
class CalendarView(View): class CalendarView(View):
""" """
Render an ICS calendar with all valid activities. Render an ICS calendar with all valid activities.

View File

@ -8,6 +8,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -57,6 +58,7 @@ class ProfileForm(forms.ModelForm):
self.fields['address'].widget.attrs.update({"placeholder": "4 avenue des Sciences, 91190 GIF-SUR-YVETTE"}) self.fields['address'].widget.attrs.update({"placeholder": "4 avenue des Sciences, 91190 GIF-SUR-YVETTE"})
self.fields['promotion'].widget.attrs.update({"max": timezone.now().year}) self.fields['promotion'].widget.attrs.update({"max": timezone.now().year})
@transaction.atomic
def save(self, commit=True): def save(self, commit=True):
if not self.instance.section or (("department" in self.changed_data if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data): or "promotion" in self.changed_data) and "section" not in self.changed_data):
@ -148,6 +150,7 @@ class ClubForm(forms.ModelForm):
"membership_fee_unpaid": AmountInput(), "membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete( "parent_club": Autocomplete(
Club, Club,
resetable=True,
attrs={ attrs={
'api_url': '/api/members/club/', 'api_url': '/api/members/club/',
} }
@ -161,7 +164,7 @@ class MembershipForm(forms.ModelForm):
soge = forms.BooleanField( soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"), label=_("Inscription paid by Société Générale"),
required=False, required=False,
help_text=_("Check this case is the Société Générale paid the inscription."), help_text=_("Check this case if the Société Générale paid the inscription."),
) )
credit_type = forms.ModelChoiceField( credit_type = forms.ModelChoiceField(

View File

@ -7,6 +7,7 @@ def create_bde_and_kfet(apps, schema_editor):
""" """
Club = apps.get_model("member", "club") Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub") NoteClub = apps.get_model("note", "noteclub")
Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
@ -45,6 +46,19 @@ def create_bde_and_kfet(apps, schema_editor):
polymorphic_ctype_id=polymorphic_ctype_id, polymorphic_ctype_id=polymorphic_ctype_id,
) )
Alias.objects.get_or_create(
id=5,
note_id=5,
name="BDE",
normalized_name="bde",
)
Alias.objects.get_or_create(
id=6,
note_id=6,
name="Kfet",
normalized_name="kfet",
)
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [

View File

@ -7,7 +7,7 @@ import os
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.template import loader from django.template import loader
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -271,6 +271,7 @@ class Club(models.Model):
self._force_save = True self._force_save = True
self.save(force_update=True) self.save(force_update=True)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): update_fields=None):
if not self.require_memberships: if not self.require_memberships:
@ -406,6 +407,7 @@ 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): def save(self, *args, **kwargs):
""" """
Calculate fee and end date before saving the membership and creating the transaction if needed. Calculate fee and end date before saving the membership and creating the transaction if needed.
@ -475,8 +477,13 @@ class Membership(models.Model):
# to treasurers. # to treasurers.
transaction.valid = False transaction.valid = False
from treasury.models import SogeCredit from treasury.models import SogeCredit
soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0] if SogeCredit.objects.filter(user=self.user).exists():
soge_credit.refresh_from_db() soge_credit = SogeCredit.objects.get(user=self.user)
else:
soge_credit = SogeCredit(user=self.user)
soge_credit._force_save = True
soge_credit.save(force_insert=True)
soge_credit.refresh_from_db()
transaction.save(force_insert=True) transaction.save(force_insert=True)
transaction.refresh_from_db() transaction.refresh_from_db()
soge_credit.transactions.add(transaction) soge_credit.transactions.add(transaction)

View File

@ -112,7 +112,7 @@ class MembershipTable(tables.Table):
fee=0, fee=0,
) )
if PermissionBackend.check_perm(get_current_authenticated_user(), 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})
t = format_html( t = format_html(

View File

@ -13,15 +13,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if additional_fee_renewal %} {% if additional_fee_renewal %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% if renewal %} {% if renewal %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} {% if club.name == "Kfet" %} {# Auto-renewal #}
The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }} {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
will be charged to renew automatically the membership in this/these club·s. The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }}
{% endblocktrans %} will be charged to renew automatically the membership in this/these club·s.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
The user is not a member of the club·s {{ clubs }}. Please create the required memberships,
otherwise it will fail.
{% endblocktrans %}
{% endif %}
{% else %} {% else %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} {% if club.name == "Kfet" %}
This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }} {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
will be charged to adhere automatically to this/these club·s. This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }}
{% endblocktrans %} will be charged to adhere automatically to this/these club·s.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
This club has parents {{ clubs }}. Please make sure that the user is a member of this or these club·s,
otherwise the creation of this membership will fail.
{% endblocktrans %}
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@ -38,7 +38,7 @@
<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>
{% if "note.view_note"|has_perm:user_object.note %} {% 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>
@ -47,7 +47,7 @@
{% endif %} {% endif %}
</dl> </dl>
{% if user_object.pk == user_object.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>{% trans 'API token' %} <i class="fa fa-cogs"></i>{% trans 'API token' %}

View File

View File

@ -0,0 +1,22 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django import template
from django.contrib.auth.models import User
from ..models import Club, Membership
def is_member(user, club):
if isinstance(user, str):
club = User.objects.get(username=user)
if isinstance(club, str):
club = Club.objects.get(name=club)
return Membership.objects\
.filter(user=user, club=club, date_start__lte=date.today(), date_end__gte=date.today()).exists()
register = template.Library()
register.filter("is_member", is_member)

View File

@ -41,7 +41,7 @@ class TemplateLoggedInTests(TestCase):
password="adminadmin", password="adminadmin",
permission_mask=3, permission_mask=3,
)) ))
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200) self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self): def test_logout(self):
response = self.client.get(reverse("logout")) response = self.client.get(reverse("logout"))

View File

@ -205,7 +205,7 @@ class TestMemberships(TestCase):
first_name="Toto", first_name="Toto",
bank="Le matelas", bank="Le matelas",
)) ))
self.assertRedirects(response, club.get_absolute_url(), 302, 200) self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=club).exists()) self.assertTrue(Membership.objects.filter(user=user, club=club).exists())
@ -244,9 +244,9 @@ class TestMemberships(TestCase):
first_name="Toto", first_name="Toto",
bank="Bank", bank="Bank",
)) ))
self.assertRedirects(response, club.get_absolute_url(), 302, 200) self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
response = self.client.get(user.profile.get_absolute_url()) response = self.client.get(club.get_absolute_url())
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_auto_join_kfet_when_join_bde_with_soge(self): def test_auto_join_kfet_when_join_bde_with_soge(self):
@ -273,7 +273,7 @@ class TestMemberships(TestCase):
first_name="Toto", first_name="Toto",
bank="Société générale", bank="Société générale",
)) ))
self.assertRedirects(response, bde.get_absolute_url(), 302, 200) self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=bde).exists()) self.assertTrue(Membership.objects.filter(user=user, club=bde).exists())
self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists()) self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists())

View File

@ -38,6 +38,7 @@ class CustomLoginView(LoginView):
""" """
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
logout(self.request) logout(self.request)
_set_current_user_and_ip(form.get_user(), self.request.session, None) _set_current_user_and_ip(form.get_user(), self.request.session, None)
@ -76,6 +77,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return context return context
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
Check if ProfileForm is correct Check if ProfileForm is correct
@ -155,8 +157,12 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
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())\ club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("club__name", "-date_start")
# Display only the most recent membership
club_list = club_list.distinct("club__name")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
membership_table = MembershipTable(data=club_list, prefix='membership-') membership_table = MembershipTable(data=club_list, prefix='membership-')
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1)) membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table context['club_list'] = membership_table
@ -164,6 +170,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# Check permissions to see if the authenticated user can lock/unlock the note # Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic(): with transaction.atomic():
modified_note = NoteUser.objects.get(pk=user.note.pk) modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = True modified_note.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\
@ -176,6 +184,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_force_lock"] = user.note.is_active and PermissionBackend\ context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_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.save() old_note.save()
modified_note.refresh_from_db() modified_note.refresh_from_db()
modified_note.is_active = True modified_note.is_active = True
@ -269,6 +278,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
self.object = self.get_object() self.object = self.get_object()
return self.form_valid(form) if form.is_valid() else self.form_invalid(form) return self.form_valid(form) if form.is_valid() else self.form_invalid(form)
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
"""Save image to note""" """Save image to note"""
image = form.cleaned_data['image'] image = form.cleaned_data['image']
@ -389,7 +399,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): if PermissionBackend.check_perm(self.request.user, "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())\
.order_by('user__last_name').all() .order_by('user__last_name').all()
context["managers"] = ClubManagerTable(data=managers, prefix="managers-") context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history # transaction history
@ -402,8 +413,12 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# member list # member list
club_member = Membership.objects.filter( club_member = Membership.objects.filter(
club=club, club=club,
date_end__gte=date.today(), date_end__gte=date.today() - timedelta(days=15),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
.order_by("user__username", "-date_start")
# Display only the most recent membership
club_member = club_member.distinct("user__username")\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
membership_table = MembershipTable(data=club_member, prefix="membership-") membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1)) membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
@ -607,6 +622,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
bank = form.cleaned_data["bank"] 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:
credit_amount = 0
if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter( if not soge and user.note.balance + credit_amount < fee and not Membership.objects.filter(
club__name="Kfet", club__name="Kfet",
user=user, user=user,
@ -628,6 +646,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
form.add_error('user', _('User is already a member of the club')) form.add_error('user', _('User is already a member of the club'))
error = True error = True
# Must join the parent club before joining this club, except for the Kfet club where it can be at the same time.
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user,
club=club.parent_club,
date_start__gte=club.parent_club.membership_start,
date_end__lte=club.parent_club.membership_end,
).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True
if club.membership_start and form.instance.date_start < club.membership_start: if club.membership_start and form.instance.date_start < club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start)) .format(form.instance.club.membership_start))
@ -650,6 +678,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return not error return not error
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
Create membership, check that all is good, make transactions Create membership, check that all is good, make transactions
@ -659,6 +688,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, 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
else: # get from url for renewal else: # get from url for renewal
old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club club = old_membership.club
@ -733,6 +763,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \ member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \ if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all() if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before
if old_membership:
member_role = member_role.union(old_membership.roles.all())
form.instance.roles.set(member_role) form.instance.roles.set(member_role)
form.instance._force_save = True form.instance._force_save = True
form.instance.save() form.instance.save()
@ -770,7 +803,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return ret return ret
def get_success_url(self): def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2020 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.db.models import Q from django.db.models import Q
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -56,8 +57,9 @@ class AliasViewSet(ReadProtectedModelViewSet):
""" """
queryset = Alias.objects.all() queryset = Alias.objects.all()
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, 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 = ['note']
ordering_fields = ['name', 'normalized_name'] ordering_fields = ['name', 'normalized_name']
def get_serializer_class(self): def get_serializer_class(self):
@ -106,8 +108,9 @@ class AliasViewSet(ReadProtectedModelViewSet):
class ConsumerViewSet(ReadOnlyProtectedModelViewSet): class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
queryset = Alias.objects.all() queryset = Alias.objects.all()
serializer_class = ConsumerSerializer serializer_class = ConsumerSerializer
filter_backends = [SearchFilter, OrderingFilter] 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 = ['note']
ordering_fields = ['name', 'normalized_name'] ordering_fields = ['name', 'normalized_name']
def get_queryset(self): def get_queryset(self):
@ -116,29 +119,31 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
:return: The filtered set of requested aliases :return: The filtered set of requested aliases
""" """
queryset = super().get_queryset() queryset = super().get_queryset().distinct()
# Sqlite doesn't support ORDER BY in subqueries # Sqlite doesn't support ORDER BY in subqueries
queryset = queryset.order_by("name") \ queryset = queryset.order_by("name") \
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", ".*") alias = self.request.query_params.get("alias", None)
queryset = queryset.prefetch_related('note') queryset = queryset.prefetch_related('note')
# We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias. if alias:
queryset = queryset.filter( # We match first an alias if it is matched without normalization,
name__iregex="^" + alias # then if the normalized pattern matches a normalized alias.
).union( queryset = queryset.filter(
queryset.filter( name__iregex="^" + alias
Q(normalized_name__iregex="^" + Alias.normalize(alias)) ).union(
& ~Q(name__iregex="^" + alias) queryset.filter(
), Q(normalized_name__iregex="^" + Alias.normalize(alias))
all=True).union( & ~Q(name__iregex="^" + alias)
queryset.filter( ),
Q(normalized_name__iregex="^" + alias.lower()) all=True).union(
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias)) queryset.filter(
& ~Q(name__iregex="^" + alias) Q(normalized_name__iregex="^" + alias.lower())
), & ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
all=True) & ~Q(name__iregex="^" + alias)
),
all=True)
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \ queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
else queryset.order_by("name") else queryset.order_by("name")
@ -179,8 +184,11 @@ class TransactionViewSet(ReadProtectedModelViewSet):
""" """
queryset = Transaction.objects.order_by("-created_at").all() queryset = Transaction.objects.order_by("-created_at").all()
serializer_class = TransactionPolymorphicSerializer serializer_class = TransactionPolymorphicSerializer
filter_backends = [SearchFilter] filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
filterset_fields = ["source", "source_alias", "destination", "destination_alias", "quantity",
"polymorphic_ctype", "amount", "created_at", ]
search_fields = ['$reason', ] search_fields = ['$reason', ]
ordering_fields = ['created_at', 'amount']
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user

View File

@ -3,7 +3,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import pre_delete, pre_save, post_save
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from . import signals from . import signals
@ -17,6 +17,15 @@ class NoteConfig(AppConfig):
""" """
Define app internal signals to interact with other apps Define app internal signals to interact with other apps
""" """
pre_save.connect(
signals.pre_save_note,
sender="note.noteuser",
)
pre_save.connect(
signals.pre_save_note,
sender="note.noteclub",
)
post_save.connect( post_save.connect(
signals.save_user_note, signals.save_user_note,
sender=settings.AUTH_USER_MODEL, sender=settings.AUTH_USER_MODEL,

View File

@ -8,7 +8,7 @@ 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
from django.db import models from django.db import models, transaction
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -93,6 +93,7 @@ class Note(PolymorphicModel):
delta = timezone.now() - self.last_negative delta = timezone.now() - self.last_negative
return "{:d} jours".format(delta.days) return "{:d} jours".format(delta.days)
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
Save note with it's alias (called in polymorphic children) Save note with it's alias (called in polymorphic children)
@ -108,12 +109,16 @@ class Note(PolymorphicModel):
# Save alias # Save alias
a.note = self a.note = self
# Consider that if the name of the note could be changed, then the alias can be created.
# It does not mean that any alias can be created.
a._force_save = True
a.save(force_insert=True) a.save(force_insert=True)
else: else:
# Check if the name of the note changed without changing the normalized form of the alias # Check if the name of the note changed without changing the normalized form of the alias
alias = Alias.objects.get(normalized_name=Alias.normalize(str(self))) alias = Alias.objects.get(normalized_name=Alias.normalize(str(self)))
if alias.name != str(self): if alias.name != str(self):
alias.name = str(self) alias.name = str(self)
alias._force_save = True
alias.save() alias.save()
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
@ -154,19 +159,6 @@ class NoteUser(Note):
def pretty(self): def pretty(self):
return _("%(user)s's note") % {'user': str(self.user)} return _("%(user)s's note") % {'user': str(self.user)}
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteUser.objects.get(pk=self.pk)
super().save(*args, **kwargs)
if old_note.balance >= 0:
# Passage en négatif
self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance()
else:
super().save(*args, **kwargs)
def send_mail_negative_balance(self): 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))
@ -195,19 +187,6 @@ class NoteClub(Note):
def pretty(self): def pretty(self):
return _("Note of %(club)s club") % {'club': str(self.club)} return _("Note of %(club)s club") % {'club': str(self.club)}
def save(self, *args, **kwargs):
if self.pk and self.balance < 0:
old_note = NoteClub.objects.get(pk=self.pk)
super().save(*args, **kwargs)
if old_note.balance >= 0:
# Passage en négatif
self.last_negative = timezone.now()
self._force_save = True
self.save(*args, **kwargs)
self.send_mail_negative_balance()
else:
super().save(*args, **kwargs)
def send_mail_negative_balance(self): 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))
@ -310,6 +289,7 @@ class Alias(models.Model):
pass pass
self.normalized_name = normalized_name self.normalized_name = normalized_name
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -170,19 +170,21 @@ class Transaction(PolymorphicModel):
previous_source_balance = self.source.balance previous_source_balance = self.source.balance
previous_dest_balance = self.destination.balance previous_dest_balance = self.destination.balance
source_balance = self.source.balance source_balance = previous_source_balance
dest_balance = self.destination.balance dest_balance = previous_dest_balance
created = self.pk is None created = self.pk is None
to_transfer = self.amount * self.quantity to_transfer = self.total
if not created and not self.valid and not hasattr(self, "_force_save"): if not created:
# Revert old transaction # Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk) # We make a select for update to avoid concurrency issues
old_transaction = Transaction.objects.select_for_update().get(pk=self.pk)
# Check that nothing important changed # Check that nothing important changed
for field_name in ["source_id", "destination_id", "quantity", "amount"]: if not hasattr(self, "_force_save"):
if getattr(self, field_name) != getattr(old_transaction, field_name): for field_name in ["source_id", "destination_id", "quantity", "amount"]:
raise ValidationError(_("You can't update the {field} on a Transaction. " if getattr(self, field_name) != getattr(old_transaction, field_name):
"Please invalidate it and create one other.").format(field=field_name)) raise ValidationError(_("You can't update the {field} on a Transaction. "
"Please invalidate it and create one other.").format(field=field_name))
if old_transaction.valid == self.valid: if old_transaction.valid == self.valid:
# Don't change anything # Don't change anything
@ -215,9 +217,8 @@ class Transaction(PolymorphicModel):
# When source == destination, no money is transferred and no transaction is created # When source == destination, no money is transferred and no transaction is created
return return
# We refresh the notes with the "select for update" tag to avoid concurrency issues self.source = Note.objects.select_for_update().get(pk=self.source_id)
self.source = Note.objects.filter(pk=self.source_id).select_for_update().get() self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
self.destination = Note.objects.filter(pk=self.destination_id).select_for_update().get()
# Check that the amounts stay between big integer bounds # Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate() diff_source, diff_dest = self.validate()
@ -237,9 +238,11 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
# Save notes # Save notes
self.source.refresh_from_db()
self.source.balance += diff_source self.source.balance += diff_source
self.source._force_save = True self.source._force_save = True
self.source.save() self.source.save()
self.destination.refresh_from_db()
self.destination.balance += diff_dest self.destination.balance += diff_dest
self.destination._force_save = True self.destination._force_save = True
self.destination.save() self.destination.save()
@ -273,6 +276,7 @@ class RecurrentTransaction(Transaction):
_("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): def save(self, *args, **kwargs):
self.clean() self.clean()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@ -323,6 +327,7 @@ class SpecialTransaction(Transaction):
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")))
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean() self.clean()
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 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
def save_user_note(instance, raw, **_kwargs): def save_user_note(instance, raw, **_kwargs):
""" """
@ -25,6 +27,16 @@ def save_club_note(instance, raw, **_kwargs):
instance.note.save() instance.note.save()
def pre_save_note(instance, raw, **_kwargs):
if not raw and instance.pk and not hasattr(instance, "_no_signal") and instance.balance < 0:
from note.models import Note
old_note = Note.objects.get(pk=instance.pk)
if old_note.balance >= 0:
# Passage en négatif
instance.last_negative = timezone.now()
instance.send_mail_negative_balance()
def delete_transaction(instance, **_kwargs): def delete_transaction(instance, **_kwargs):
""" """
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first. Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.

View File

@ -29,7 +29,6 @@ $(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 () {
$('#consos_list_div').removeClass('d-none') $('#consos_list_div').removeClass('d-none')
$('#user_select_div').attr('class', 'col-xl-4')
$('#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')
@ -48,7 +47,6 @@ $(document).ready(function () {
$('#single_conso').change(function () { $('#single_conso').change(function () {
$('#consos_list_div').addClass('d-none') $('#consos_list_div').addClass('d-none')
$('#user_select_div').attr('class', 'col-xl-7')
$('#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')

View File

@ -67,7 +67,11 @@ $(document).ready(function () {
last.quantity = 1 last.quantity = 1
if (!last.note.user) { if (last.note.club) {
$('#last_name').val(last.note.name)
$('#first_name').val(last.note.name)
}
else if (!last.note.user) {
$.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) { $.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) {
last.note.user = note.user last.note.user = note.user
$.getJSON('/api/user/' + last.note.user + '/', function (user) { $.getJSON('/api/user/' + last.note.user + '/', function (user) {
@ -246,7 +250,7 @@ $('#btn_transfer').click(function () {
error = true error = true
} }
if (!reason_field.val()) { if (!reason_field.val() && $('#type_transfer').is(':checked')) {
reason_field.addClass('is-invalid') reason_field.addClass('is-invalid')
$('#reason-required').html('<strong>Ce champ est requis.</strong>') $('#reason-required').html('<strong>Ce champ est requis.</strong>')
error = true error = true
@ -388,7 +392,7 @@ $('#btn_transfer').click(function () {
alias = sources_notes_display[0].name alias = sources_notes_display[0].name
source_id = user_note.id source_id = user_note.id
dest_id = special_note dest_id = special_note
reason = 'Retrait ' + $('#credit_type option:selected').text().toLowerCase() reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase()
if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } if (given_reason.length > 0) { reason += ' (' + given_reason + ')' }
} }
$.post('/api/note/transaction/transaction/', $.post('/api/note/transaction/transaction/',

View File

@ -10,22 +10,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-sm-5 col-md-4" id="infos_div"> <div class="col-sm-5 col-md-4" id="infos_div">
<div class="row"> <div class="row justify-content-center justify-content-md-end">
{# User details column #} {# User details column #}
<div class="col"> <div class="col picture-col">
<div class="card bg-light border-success mb-4 text-center"> <div class="card bg-light mb-4 text-center">
<a id="profile_pic_link" href="#"> <a id="profile_pic_link" href="#">
<img src="{% static "member/img/default_picture.png" %}" <img src="{% static "member/img/default_picture.png" %}"
id="profile_pic" alt="" class="card-img-top"> id="profile_pic" alt="" class="card-img-top d-none d-sm-block">
</a> </a>
<div class="card-body text-center text-break"> <div class="card-body text-center text-break p-2">
<span id="user_note"></span> <span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span>
</div> </div>
</div> </div>
</div> </div>
{# User selection column #} {# User selection column #}
<div class="col-xl-7" id="user_select_div"> <div class="col-xl" id="user_select_div">
<div class="card bg-light border-success mb-4"> <div class="card bg-light border-success mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
@ -44,6 +44,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
</div> </div>
{# Summary of consumption and consume button #} {# Summary of consumption and consume button #}
<div class="col-xl-5 d-none" id="consos_list_div"> <div class="col-xl-5 d-none" id="consos_list_div">
<div class="card bg-light border-info mb-4"> <div class="card bg-light border-info mb-4">
@ -65,7 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
{# Show last used buttons #} {# Show last used buttons #}
<div class="card bg-light mb-4"> <div class="card bg-light mb-4">
<div class="card-header"> <div class="card-header">
@ -159,7 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %} {% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript" src="{% static "js/consos.js" %}"></script> <script type="text/javascript" src="{% static "note/js/consos.js" %}"></script>
<script type="text/javascript"> <script type="text/javascript">
{% for button in highlighted %} {% for button in highlighted %}
{% if button.display %} {% if button.display %}

View File

@ -34,21 +34,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
<hr> <hr>
<div class="row"> <div class="row justify-content-center">
{# Preview note profile (picture, username and balance) #} {# Preview note profile (picture, username and balance) #}
<div class="col-md-3" id="note_infos_div"> <div class="col-md picture-col" id="note_infos_div">
<div class="card bg-light border-success shadow mb-4 pt-4 text-center"> <div class="card bg-light mb-4 text-center">
<a id="profile_pic_link" href="#"><img src="{% static "member/img/default_picture.png" %}" <a id="profile_pic_link" href="#"><img src="{% static "member/img/default_picture.png" %}"
id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a> id="profile_pic" alt="" class="img-fluid rounded mx-auto"></a>
<div class="card-body text-center"> <div class="card-body text-center p-2">
<span id="user_note"></span> <span id="user_note"><i class="small">{% trans "Please select a note" %}</i></span>
</div> </div>
</div> </div>
</div> </div>
{# list of emitters #} {# list of emitters #}
<div class="col-md-3" id="emitters_div"> <div class="col-md-3" id="emitters_div">
<div class="card bg-light border-success shadow mb-4"> <div class="card bg-light mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
<label for="source_note" id="source_note_label">{% trans "Select emitters" %}</label> <label for="source_note" id="source_note_label">{% trans "Select emitters" %}</label>
@ -75,7 +75,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
{# list of receiver #} {# list of receiver #}
<div class="col-md-3" id="dests_div"> <div class="col-md-3" id="dests_div">
<div class="card bg-light border-info shadow mb-4"> <div class="card bg-light mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold" id="dest_title"> <p class="card-text font-weight-bold" id="dest_title">
<label for="dest_note" id="dest_note_label">{% trans "Select receivers" %}</label> <label for="dest_note" id="dest_note_label">{% trans "Select receivers" %}</label>
@ -97,8 +97,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
{# Information on transaction (amount, reason, name,...) #} {# Information on transaction (amount, reason, name,...) #}
<div class="col-md-3" id="external_div"> <div class="col-md" id="external_div">
<div class="card bg-light border-warning shadow mb-4"> <div class="card bg-light mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "Action" %} {% trans "Action" %}
@ -153,7 +153,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</div> </div>
</div> </div>
{# transaction history #} {# transaction history #}
<div class="card shadow mb-4" id="history"> <div class="card mb-4" id="history">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "Recent transactions history" %} {% trans "Recent transactions history" %}
@ -176,5 +176,5 @@ SPDX-License-Identifier: GPL-2.0-or-later
select_receveirs_label = "{% trans "Select receivers" %}"; select_receveirs_label = "{% trans "Select receivers" %}";
transfer_type_label = "{% trans "Transfer type" %}"; transfer_type_label = "{% trans "Transfer type" %}";
</script> </script>
<script src="/static/js/transfer.js"></script> <script src="{% static "note/js/transfer.js" %}"></script>
{% endblock %} {% endblock %}

View File

@ -144,7 +144,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
""" """
The Magic View that make people pay their beer and burgers. The Magic View that make people pay their beer and burgers.
(Most of the magic happens in the dark world of Javascript see `note_kfet/static/js/consos.js`) (Most of the magic happens in the dark world of Javascript see `static/note/js/consos.js`)
""" """
model = Transaction model = Transaction
template_name = "note/conso_form.html" template_name = "note/conso_form.html"

View File

@ -4,7 +4,6 @@
from functools import lru_cache from functools import lru_cache
from time import time from time import time
from django.conf import settings
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from note_kfet.middlewares import get_current_session from note_kfet.middlewares import get_current_session
@ -33,9 +32,9 @@ def memoize(f):
sess_funs = new_sess_funs sess_funs = new_sess_funs
def func(*args, **kwargs): def func(*args, **kwargs):
if settings.DEBUG: # if settings.DEBUG:
# Don't memoize in DEBUG mode # # Don't memoize in DEBUG mode
return f(*args, **kwargs) # return f(*args, **kwargs)
nonlocal last_collect nonlocal last_collect

View File

@ -115,7 +115,7 @@
"type": "view", "type": "view",
"mask": 1, "mask": 1,
"field": "", "field": "",
"permanent": true, "permanent": false,
"description": "Voir les aliases des notes des clubs et des adhérents du club Kfet" "description": "Voir les aliases des notes des clubs et des adhérents du club Kfet"
} }
}, },
@ -1103,7 +1103,7 @@
"treasury", "treasury",
"sogecredit" "sogecredit"
], ],
"query": "{\"credit_transaction\": null}", "query": "{}",
"type": "add", "type": "add",
"mask": 1, "mask": 1,
"field": "", "field": "",
@ -2647,6 +2647,166 @@
"description": "Changer l'image de la note de son club" "description": "Changer l'image de la note de son club"
} }
}, },
{
"model": "permission.permission",
"pk": 170,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__is_active\": true}",
"type": "add",
"mask": 1,
"field": "",
"permanent": false,
"description": "Ajouter n'importe quel alias à une note non bloquée"
}
},
{
"model": "permission.permission",
"pk": 171,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__is_active\": true}",
"type": "delete",
"mask": 3,
"field": "",
"permanent": false,
"description": "Supprimer n'importe quel alias à une note non bloquée"
}
},
{
"model": "permission.permission",
"pk": 172,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les remises"
}
},
{
"model": "permission.permission",
"pk": 173,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "add",
"mask": 3,
"field": "",
"permanent": false,
"description": "Ajouter une remise"
}
},
{
"model": "permission.permission",
"pk": 174,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "change",
"mask": 3,
"field": "",
"permanent": false,
"description": "Modifier une remise"
}
},
{
"model": "permission.permission",
"pk": 175,
"fields": {
"model": [
"treasury",
"remittance"
],
"query": "{}",
"type": "delete",
"mask": 3,
"field": "",
"permanent": false,
"description": "Supprimer une remise"
}
},
{
"model": "permission.permission",
"pk": 176,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"profile__registration_valid\": false}",
"type": "change",
"mask": 1,
"field": "",
"permanent": false,
"description": "Modifier n'importe quel utilisateur non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 177,
"fields": {
"model": [
"member",
"profile"
],
"query": "{\"registration_valid\": false}",
"type": "change",
"mask": 1,
"field": "",
"permanent": false,
"description": "Modifier n'importe quel profil non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 178,
"fields": {
"model": [
"note",
"alias"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les alias, y compris ceux des non adhérents"
}
},
{
"model": "permission.permission",
"pk": 179,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__noteuser__user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"description": "Voir ses propres alias, pour toujours"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -2717,7 +2877,8 @@
157, 157,
158, 158,
159, 159,
160 160,
179
] ]
} }
}, },
@ -2821,6 +2982,7 @@
31, 31,
32, 32,
33, 33,
51,
53, 53,
54, 54,
55, 55,
@ -2844,13 +3006,23 @@
137, 137,
138, 138,
139, 139,
140,
143, 143,
146, 146,
147, 147,
150, 150,
151, 151,
163, 163,
164 164,
170,
171,
172,
173,
174,
175,
176,
177,
178
] ]
} }
}, },
@ -3024,7 +3196,16 @@
166, 166,
167, 167,
168, 168,
169 169,
170,
171,
172,
173,
174,
175,
176,
177,
178
] ]
} }
}, },
@ -3050,10 +3231,15 @@
29, 29,
30, 30,
31, 31,
70,
143, 143,
166, 166,
167, 167,
168 168,
170,
171,
176,
177
] ]
} }
}, },
@ -3219,10 +3405,13 @@
138, 138,
139, 139,
140, 140,
143,
145, 145,
146, 146,
147, 147,
150 150,
176,
177
] ]
} }
}, },

View File

@ -43,6 +43,7 @@ class InstancedPermission:
obj = copy(obj) obj = copy(obj)
obj.pk = 0 obj.pk = 0
with transaction.atomic(): with transaction.atomic():
sid = transaction.savepoint()
for o in self.model.model_class().objects.filter(pk=0).all(): for o in self.model.model_class().objects.filter(pk=0).all():
o._force_delete = True o._force_delete = True
Model.delete(o) Model.delete(o)
@ -62,8 +63,10 @@ class InstancedPermission:
Model.save(obj, force_insert=True) Model.save(obj, force_insert=True)
ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists() ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists()
# Delete testing object # Delete testing object
obj._no_signal = True
obj._force_delete = True obj._force_delete = True
Model.delete(obj) Model.delete(obj)
transaction.savepoint_rollback(sid)
return ret return ret
@ -199,6 +202,7 @@ class Permission(models.Model):
if self.field and self.type not in {'view', 'change'}: if self.field and self.type not in {'view', 'change'}:
raise ValidationError(_("Specifying field applies only to view and change permission types.")) raise ValidationError(_("Specifying field applies only to view and change permission types."))
@transaction.atomic
def save(self, **kwargs): def save(self, **kwargs):
self.full_clean() self.full_clean()
super().save() super().save()

View File

@ -14,6 +14,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
This is a simple patch of this class that controls view access. This is a simple patch of this class that controls view access.
""" """
# The queryset is filtered, and permissions are more powerful than a simple check than just "can view this model"
perms_map = { perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'], 'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': [], 'OPTIONS': [],

View File

@ -6,6 +6,7 @@ from datetime import date
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import HiddenInput from django.forms import HiddenInput
from django.http import Http404 from django.http import Http404
@ -56,6 +57,7 @@ class ProtectQuerysetMixin:
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
Submit the form, if the page is a FormView. Submit the form, if the page is a FormView.

View File

@ -44,6 +44,15 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', ) fields = ('first_name', 'last_name', 'username', 'email', )
class DeclareSogeAccountOpenedForm(forms.Form):
soge_account = forms.BooleanField(
label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."),
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
"account, you will have to pay the BDE membership."),
required=False,
)
class WEISignupForm(forms.Form): class WEISignupForm(forms.Form):
wei_registration = forms.BooleanField( wei_registration = forms.BooleanField(
label=_("Register to the WEI"), label=_("Register to the WEI"),
@ -60,7 +69,7 @@ class ValidationForm(forms.Form):
soge = forms.BooleanField( soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"), label=_("Inscription paid by Société Générale"),
required=False, required=False,
help_text=_("Check this case is the Société Générale paid the inscription."), help_text=_("Check this case if the Société Générale paid the inscription."),
) )
credit_type = forms.ModelChoiceField( credit_type = forms.ModelChoiceField(

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-md-3 mb-4"> <div class="col-xl-5 mb-4">
<div class="card bg-light shadow"> <div class="card bg-light shadow">
<div class="card-header text-center" > <div class="card-header text-center" >
<h4> {% trans "Account #" %} {{ object.pk }}</h4> <h4> {% trans "Account #" %} {{ object.pk }}</h4>
@ -50,12 +50,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-9"> <div class="col-md-7">
<div class="card bg-light shadow"> <div class="card bg-light shadow">
<form method="post"> <form method="post">
<div class="card-header text-center" > <div class="card-header text-center" >
<h4> {% trans "Validate account" %}</h4> <h4> {% trans "Validate account" %}</h4>
</div> </div>
{% if declare_soge_account %}
<div class="alert alert-info">
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
</div>
{% endif %}
<div class="card-body" id="profile_infos"> <div class="card-body" id="profile_infos">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
@ -104,7 +111,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
soge_field.change(fillFields); soge_field.change(fillFields);
{% if object.profile.soge %} {% if declare_soge_account %}
soge_field.attr('checked', true); soge_field.attr('checked', true);
fillFields(); fillFields();
{% endif %} {% endif %}

View File

@ -5,6 +5,7 @@ from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.shortcuts import resolve_url, redirect from django.shortcuts import resolve_url, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -23,7 +24,7 @@ from permission.models import Role
from permission.views import ProtectQuerysetMixin from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit from treasury.models import SogeCredit
from .forms import SignUpForm, ValidationForm from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .tables import FutureUserTable from .tables import FutureUserTable
from .tokens import email_validation_token from .tokens import email_validation_token
@ -41,12 +42,14 @@ class UserCreateView(CreateView):
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["profile_form"] = self.second_form(self.request.POST if self.request.POST else None) context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
del context["profile_form"].fields["section"] del context["profile_form"].fields["section"]
del context["profile_form"].fields["report_frequency"] del context["profile_form"].fields["report_frequency"]
del context["profile_form"].fields["last_report"] del context["profile_form"].fields["last_report"]
return context return context
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
If the form is valid, then the user is created with is_active set to False If the form is valid, then the user is created with is_active set to False
@ -70,6 +73,13 @@ class UserCreateView(CreateView):
user.profile.send_email_validation_link() user.profile.send_email_validation_link()
soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
soge_credit = SogeCredit(user=user)
soge_credit._force_save = True
soge_credit.save()
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -180,7 +190,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
| Q(username__iregex="^" + pattern) | Q(username__iregex="^" + pattern)
) )
return qs[:20] return qs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -225,6 +235,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += 8000 fee += 8000
ctx["total_fee"] = "{:.02f}".format(fee / 100, ) ctx["total_fee"] = "{:.02f}".format(fee / 100, )
ctx["declare_soge_account"] = True
return ctx return ctx
def get_form(self, form_class=None): def get_form(self, form_class=None):
@ -234,6 +246,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form.fields["first_name"].initial = user.first_name form.fields["first_name"].initial = user.first_name
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
user = self.get_object() user = self.get_object()
@ -304,6 +317,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user.profile.save() user.profile.save()
user.refresh_from_db() user.refresh_from_db()
if not soge and SogeCredit.objects.filter(user=user).exists():
# If the user declared that a bank account was opened but in the validation form the SoGé case was
# unchecked, delete the associated credit
soge_credit = SogeCredit.objects.get(user=user)
soge_credit._force_delete = True
soge_credit.delete()
if credit_type is not None and credit_amount > 0: if credit_type is not None and credit_amount > 0:
# Credit the note # Credit the note
SpecialTransaction.objects.create( SpecialTransaction.objects.create(
@ -370,6 +390,8 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
user = User.objects.filter(profile__registration_valid=False)\ user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\ .filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"]) .get(pk=self.kwargs["pk"])
# Delete associated soge credits before
SogeCredit.objects.filter(user=user).delete()
user.delete() user.delete()

View File

@ -4,6 +4,7 @@
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit from crispy_forms.layout import Submit
from django import forms from django import forms
from django.db import transaction
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import AmountInput from note_kfet.inputs import AmountInput
@ -149,6 +150,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
self.instance.transaction.bank = cleaned_data["bank"] self.instance.transaction.bank = cleaned_data["bank"]
return cleaned_data return cleaned_data
@transaction.atomic
def save(self, commit=True): def save(self, commit=True):
""" """
Save the transaction and the remittance. Save the transaction and the remittance.

View File

@ -5,12 +5,12 @@ from datetime import date
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
class Invoice(models.Model): class Invoice(models.Model):
@ -76,6 +76,7 @@ class Invoice(models.Model):
verbose_name=_("tex source"), verbose_name=_("tex source"),
) )
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
When an invoice is generated, we store the tex source. When an invoice is generated, we store the tex source.
@ -228,6 +229,7 @@ class Remittance(models.Model):
""" """
return sum(transaction.total for transaction in self.transactions.all()) return sum(transaction.total for transaction in self.transactions.all())
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None): def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# Check if all transactions have the right type. # Check if all transactions have the right type.
if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): if self.transactions.exists() and self.transactions.filter(~Q(source=self.remittance_type.note)).exists():
@ -291,11 +293,12 @@ class SogeCredit(models.Model):
@property @property
def valid(self): def valid(self):
return self.credit_transaction.valid return self.credit_transaction and self.credit_transaction.valid
@property @property
def amount(self): def amount(self):
return sum(transaction.total for transaction in self.transactions.all()) + 8000 return self.credit_transaction.total if self.valid \
else sum(transaction.total for transaction in self.transactions.all()) + 8000
def invalidate(self): def invalidate(self):
""" """
@ -305,10 +308,10 @@ class SogeCredit(models.Model):
if self.valid: if self.valid:
self.credit_transaction.valid = False self.credit_transaction.valid = False
self.credit_transaction.save() self.credit_transaction.save()
for transaction in self.transactions.all(): for tr in self.transactions.all():
transaction.valid = False tr.valid = False
transaction._force_save = True tr._force_save = True
transaction.save() tr.save()
def validate(self, force=False): def validate(self, force=False):
if self.valid and not force: if self.valid and not force:
@ -320,18 +323,25 @@ class SogeCredit(models.Model):
# Refresh credit amount # Refresh credit amount
self.save() self.save()
self.credit_transaction.valid = True self.credit_transaction.valid = True
self.credit_transaction._force_save = True
self.credit_transaction.save() self.credit_transaction.save()
self.save() self.save()
for transaction in self.transactions.all(): for tr in self.transactions.all():
transaction.valid = True tr.valid = True
transaction._force_save = True tr._force_save = True
transaction.created_at = timezone.now() tr.created_at = timezone.now()
transaction.save() tr.save()
@transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# This is a pre-registered user that declared that a SoGé account was opened.
# No note exists yet.
if not NoteUser.objects.filter(user=self.user).exists():
return super().save(*args, **kwargs)
if not self.credit_transaction: if not self.credit_transaction:
self.credit_transaction = SpecialTransaction.objects.create( credit_transaction = SpecialTransaction(
source=NoteSpecial.objects.get(special_type="Virement bancaire"), source=NoteSpecial.objects.get(special_type="Virement bancaire"),
destination=self.user.note, destination=self.user.note,
quantity=1, quantity=1,
@ -342,6 +352,10 @@ class SogeCredit(models.Model):
bank="Société générale", bank="Société générale",
valid=False, valid=False,
) )
credit_transaction._force_save = True
credit_transaction.save()
credit_transaction.refresh_from_db()
self.credit_transaction = credit_transaction
elif not self.valid: elif not self.valid:
self.credit_transaction.amount = self.amount self.credit_transaction.amount = self.amount
self.credit_transaction._force_save = True self.credit_transaction._force_save = True
@ -361,11 +375,11 @@ class SogeCredit(models.Model):
"Please ask her/him to credit the note before invalidating this credit.")) "Please ask her/him to credit the note before invalidating this credit."))
self.invalidate() self.invalidate()
for transaction in self.transactions.all(): for tr in self.transactions.all():
transaction._force_save = True tr._force_save = True
transaction.valid = True tr.valid = True
transaction.created_at = timezone.now() tr.created_at = timezone.now()
transaction.save() tr.save()
self.credit_transaction.valid = False self.credit_transaction.valid = False
self.credit_transaction.reason += " (invalide)" self.credit_transaction.reason += " (invalide)"
self.credit_transaction.save() self.credit_transaction.save()

View File

@ -60,7 +60,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
let pattern = searchbar_obj.val(); let pattern = searchbar_obj.val();
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + ( $("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table"); invalid_only_obj.is(':checked') ? "" : "&valid=1") + " #credits_table");
$(".table-row").click(function () { $(".table-row").click(function () {
window.document.location = $(this).data("href"); window.document.location = $(this).data("href");

View File

@ -9,6 +9,7 @@ from tempfile import mkdtemp
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError, PermissionDenied from django.core.exceptions import ValidationError, PermissionDenied
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import Form from django.forms import Form
from django.http import HttpResponse from django.http import HttpResponse
@ -65,6 +66,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
del form.fields["locked"] del form.fields["locked"]
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
ret = super().form_valid(form) ret = super().form_valid(form)
@ -144,6 +146,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
del form.fields["id"] del form.fields["id"]
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
ret = super().form_valid(form) ret = super().form_valid(form)
@ -428,7 +431,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
if "valid" not in self.request.GET or not self.request.GET["valid"]: if "valid" not in self.request.GET or not self.request.GET["valid"]:
qs = qs.filter(credit_transaction__valid=False) qs = qs.filter(credit_transaction__valid=False)
return qs[:20] return qs
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView): class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):
@ -439,6 +442,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie
form_class = Form form_class = Form
extra_context = {"title": _("Manage credits from the Société générale")} extra_context = {"title": _("Manage credits from the Société générale")}
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
if "validate" in form.data: if "validate" in form.data:
self.get_object().validate(True) self.get_object().validate(True)

View File

@ -4,6 +4,7 @@
from random import choice from random import choice
from django import forms from django import forms
from django.db import transaction
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
@ -88,6 +89,7 @@ class WEISurvey2020(WEISurvey):
""" """
form.set_registration(self.registration) form.set_registration(self.registration)
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
word = form.cleaned_data["word"] word = form.cleaned_data["word"]
self.information.step += 1 self.information.step += 1

View File

@ -1,59 +0,0 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django.core.management import BaseCommand
from django.db.models import Q
from member.models import Membership, Club
from wei.models import WEIClub
class Command(BaseCommand):
help = "Get mailing list registrations from the last wei. " \
"Usage: manage.py extract_ml_registrations -t {events,art,sport}. " \
"You can write this into a file with a pipe, then paste the document into your mail manager."
def add_arguments(self, parser):
parser.add_argument('--type', '-t', choices=["members", "clubs", "events", "art", "sport"], default="members",
help='Select the type of the mailing list (default members)')
parser.add_argument('--year', '-y', type=int, default=None,
help='Select the year of the concerned WEI. Default: last year')
def handle(self, *args, **options):
###########################################################
# WARNING #
###########################################################
#
# This code is obsolete.
# TODO: Improve the mailing list extraction system, and link it automatically with Mailman.
if options["type"] == "members":
for membership in Membership.objects.filter(
club__name="BDE",
date_start__lte=date.today(),
date_end__gte=date.today(),
).all():
self.stdout.write(membership.user.email)
return
if options["type"] == "clubs":
for club in Club.objects.all():
self.stdout.write(club.email)
return
if options["year"] is None:
wei = WEIClub.objects.order_by('-year').first()
else:
wei = WEIClub.objects.filter(year=options["year"])
if wei.exists():
wei = wei.get()
else:
wei = WEIClub.objects.order_by('-year').first()
self.stderr.write(self.style.WARNING("Warning: there was no WEI in year " + str(options["year"]) + ". "
+ "Assuming the last WEI (year " + str(wei.year) + ")"))
q = Q(ml_events_registration=True) if options["type"] == "events" else Q(ml_art_registration=True)\
if options["type"] == "art" else Q(ml_sport_registration=True)
registrations = wei.users.filter(q)
for registration in registrations.all():
self.stdout.write(registration.user.email)

View File

@ -238,7 +238,7 @@ class WEIRegistration(models.Model):
information_json = models.TextField( information_json = models.TextField(
default="{}", default="{}",
verbose_name=_("registration information"), verbose_name=_("registration information"),
help_text=_("Information about the registration (buses for old members, survey fot the new members), " help_text=_("Information about the registration (buses for old members, survey for the new members), "
"encoded in JSON"), "encoded in JSON"),
) )

View File

@ -10,6 +10,7 @@ from tempfile import mkdtemp
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, Count from django.db.models import Q, Count
from django.db.models.functions.text import Lower from django.db.models.functions.text import Lower
from django.forms import HiddenInput from django.forms import HiddenInput
@ -84,6 +85,7 @@ class WEICreateView(ProtectQuerysetMixin, ProtectedCreateView):
date_end=date.today(), date_end=date.today(),
) )
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.requires_membership = True form.instance.requires_membership = True
form.instance.parent_club = Club.objects.get(name="Kfet") form.instance.parent_club = Club.objects.get(name="Kfet")
@ -517,6 +519,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, ProtectedCreateView):
del form.fields["information_json"] del form.fields["information_json"]
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
form.instance.first_year = True form.instance.first_year = True
@ -597,6 +600,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, ProtectedCreateView):
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
form.instance.first_year = False form.instance.first_year = False
@ -688,6 +692,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
del form.fields["information_json"] del form.fields["information_json"]
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
# If the membership is already validated, then we update the bus and the team (and the roles) # If the membership is already validated, then we update the bus and the team (and the roles)
if form.instance.is_validated: if form.instance.is_validated:
@ -866,6 +871,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
).all() ).all()
return form return form
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
Create membership, check that all is good, make transactions Create membership, check that all is good, make transactions
@ -1016,6 +1022,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
context["club"] = self.object.wei context["club"] = self.object.wei
return context return context
@transaction.atomic
def form_valid(self, form): def form_valid(self, form):
""" """
Update the survey with the data of the form. Update the survey with the data of the form.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,3 +20,5 @@
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports 55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports
# Mettre à jour les boutons mis en avant # Mettre à jour les boutons mis en avant
00 9 * * * root cd /var/www/note_kfet && env/bin/python manage.py refresh_highlighted_buttons 00 9 * * * root cd /var/www/note_kfet && env/bin/python manage.py refresh_highlighted_buttons
# Vider les tokens Oauth2
00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens

View File

@ -26,6 +26,14 @@ admin_site = StrongAdminSite()
admin_site.register(Site, SiteAdmin) admin_site.register(Site, SiteAdmin)
# Add external apps model # Add external apps model
if "oauth2_provider" in settings.INSTALLED_APPS:
from oauth2_provider.admin import Application, ApplicationAdmin, Grant, \
GrantAdmin, AccessToken, AccessTokenAdmin, RefreshToken, RefreshTokenAdmin
admin_site.register(Application, ApplicationAdmin)
admin_site.register(Grant, GrantAdmin)
admin_site.register(AccessToken, AccessTokenAdmin)
admin_site.register(RefreshToken, RefreshTokenAdmin)
if "django_htcpcp_tea" in settings.INSTALLED_APPS: if "django_htcpcp_tea" in settings.INSTALLED_APPS:
from django_htcpcp_tea.admin import * from django_htcpcp_tea.admin import *
from django_htcpcp_tea.models import * from django_htcpcp_tea.models import *
@ -44,9 +52,3 @@ if "rest_framework" in settings.INSTALLED_APPS:
from rest_framework.authtoken.admin import * from rest_framework.authtoken.admin import *
from rest_framework.authtoken.models import * from rest_framework.authtoken.models import *
admin_site.register(Token, TokenAdmin) admin_site.register(Token, TokenAdmin)
if "cas_server" in settings.INSTALLED_APPS:
from cas_server.admin import *
from cas_server.models import *
admin_site.register(ServicePattern, ServicePatternAdmin)
admin_site.register(FederatedIendityProvider, FederatedIendityProviderAdmin)

View File

@ -1,11 +0,0 @@
[
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 1,
"pattern": ".*",
"name": "REPLACEME"
}
}
]

View File

@ -3,7 +3,7 @@
"model": "sites.site", "model": "sites.site",
"pk": 1, "pk": 1,
"fields": { "fields": {
"domain": "localhost", "domain": "note.crans.org",
"name": "La Note Kfet \ud83c\udf7b" "name": "La Note Kfet \ud83c\udf7b"
} }
} }

View File

@ -50,6 +50,20 @@ class SessionMiddleware(object):
def __call__(self, request): def __call__(self, request):
user = request.user user = request.user
# If we authenticate through a token to connect to the API, then we query the good user
if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"):
token = request.META.get('HTTP_AUTHORIZATION')
if token.startswith("Token "):
token = token[6:]
from rest_framework.authtoken.models import Token
if Token.objects.filter(key=token).exists():
token_obj = Token.objects.get(key=token)
user = token_obj.user
session = request.session
session["permission_mask"] = 42
session.save()
if 'HTTP_X_REAL_IP' in request.META: if 'HTTP_X_REAL_IP' in request.META:
ip = request.META.get('HTTP_X_REAL_IP') ip = request.META.get('HTTP_X_REAL_IP')
elif 'HTTP_X_FORWARDED_FOR' in request.META: elif 'HTTP_X_FORWARDED_FOR' in request.META:

View File

@ -49,13 +49,6 @@ try:
except ImportError: except ImportError:
pass pass
if "cas_server" in INSTALLED_APPS:
# CAS Settings
CAS_AUTO_CREATE_USER = False
CAS_LOGO_URL = "/static/img/Saperlistpopette.png"
CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png"
CAS_SHOW_POWERED = False
if "logs" in INSTALLED_APPS: if "logs" in INSTALLED_APPS:
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)

View File

@ -35,8 +35,10 @@ INSTALLED_APPS = [
'mailer', 'mailer',
'phonenumber_field', 'phonenumber_field',
'polymorphic', 'polymorphic',
'oauth2_provider',
# Django contrib # Django contrib
# Django Admin will autodiscover our apps for our custom admin site.
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.admindocs', 'django.contrib.admindocs',
'django.contrib.auth', 'django.contrib.auth',
@ -154,6 +156,7 @@ from django.utils.translation import gettext_lazy as _
LANGUAGES = [ LANGUAGES = [
('de', _('German')), ('de', _('German')),
('en', _('English')), ('en', _('English')),
('es', _('Spanish')),
('fr', _('French')), ('fr', _('French')),
] ]
@ -213,6 +216,16 @@ EMAIL_HOST_PASSWORD = os.getenv('EMAIL_PASSWORD', None)
SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com") SERVER_EMAIL = os.getenv("NOTE_MAIL", "notekfet@example.com")
DEFAULT_FROM_EMAIL = "NoteKfet2020 <" + SERVER_EMAIL + ">" DEFAULT_FROM_EMAIL = "NoteKfet2020 <" + SERVER_EMAIL + ">"
# Cache
# https://docs.djangoproject.com/en/2.2/topics/cache/#setting-up-the-cache
cache_address = os.getenv("CACHE_ADDRESS", "127.0.0.1:11211")
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': cache_address,
}
}
# Django REST Framework # Django REST Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
@ -232,7 +245,7 @@ REST_FRAMEWORK = {
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
# After login redirect user to transfer page # After login redirect user to transfer page
LOGIN_REDIRECT_URL = '/note/transfer/' LOGIN_REDIRECT_URL = '/'
# An user session will expired after 3 hours # An user session will expired after 3 hours
SESSION_COOKIE_AGE = 60 * 60 * 3 SESSION_COOKIE_AGE = 60 * 60 * 3

View File

@ -24,6 +24,14 @@ if os.getenv("DJANGO_DEV_STORE_METHOD", "sqlite") != "postgresql":
} }
} }
# Dummy cache for development
# https://docs.djangoproject.com/en/2.2/topics/cache/#setting-up-the-cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
# Break it, fix it! # Break it, fix it!
DEBUG = True DEBUG = True

View File

@ -3,7 +3,6 @@
# CAS # CAS
OPTIONAL_APPS = [ OPTIONAL_APPS = [
# 'cas_server',
# 'debug_toolbar' # 'debug_toolbar'
] ]

View File

@ -22,6 +22,11 @@
border-bottom-color: rgba(0, 0, 0, .250); border-bottom-color: rgba(0, 0, 0, .250);
} }
/* Fixed width picture column */
.picture-col {
max-width: 202px;
}
/* Limit fluid container to a max size */ /* Limit fluid container to a max size */
.container-fluid { .container-fluid {
max-width: 1600px; max-width: 1600px;

View File

@ -1,4 +1,4 @@
{% load static i18n pretty_money static getenv perms %} {% load static i18n pretty_money static getenv perms memberinfo %}
{% comment %} {% comment %}
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
@ -64,7 +64,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li> </li>
{% endif %} {% endif %}
{% if "note.transaction"|not_empty_model_list %} {% if user.is_authenticated and user|is_member:"Kfet" %}
<li class="nav-item"> <li class="nav-item">
{% url 'note:transfer' as url %} {% url 'note:transfer' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %} </a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %} </a>
@ -150,12 +150,36 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</nav> </nav>
<div class="{% block containertype %}container{% endblock %} my-3"> <div class="{% block containertype %}container{% endblock %} my-3">
{% if request.user.is_authenticated and not request.user.profile.email_confirmed %} <div id="messages">
<div class="alert alert-warning"> {% if user.is_authenticated %}
{% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %} {% if not user|is_member:"BDE" %}
</div> <div class="alert alert-danger">
{% endif %} {% trans "You are not a BDE member anymore. Please renew your membership if you want to use the note." %}
<div id="messages"></div> </div>
{% elif not user|is_member:"Kfet" %}
<div class="alert alert-warning">
{% trans "You are not a Kfet member, so you can't use your note account." %}
</div>
{% endif %}
{% if not user.profile.email_confirmed %}
<div class="alert alert-warning">
{% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %}
</div>
{% endif %}
{% endif %}
{% if user.sogecredit and not user.sogecredit.valid %}
<div class="alert alert-info">
{% blocktrans trimmed %}
You declared that you opened a bank account in the Société générale. The bank did not validate the creation of the account to the BDE,
so the registration bonus of 80 € is not credited and the membership is not paid yet.
This verification procedure may last a few days.
Please make sure that you go to the end of the account creation.
{% endblocktrans %}
</div>
{% endif %}
{# TODO Add banners #}
</div>
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>
{% endblock %} {% endblock %}
@ -177,12 +201,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
onchange="this.form.submit()"> onchange="this.form.submit()">
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %} {% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %} {% for lang_code, lang_name in LANGUAGES %}
{% for language in languages %} <option value="{{ lang_code }}"
<option value="{{ language.code }}" {% if lang_code == LANGUAGE_CODE %}
{% if language.code == LANGUAGE_CODE %}
selected{% endif %}> selected{% endif %}>
{{ language.name_local }} ({{ language.code }}) {{ lang_name }} ({{ lang_code }})
</option> </option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,99 +0,0 @@
{% load i18n %}{% load static %}{% get_current_language as LANGUAGE_CODE %}<!DOCTYPE html>
<html{% if LANGUAGE_CODE %} lang="{{LANGUAGE_CODE}}"{% endif %}>
<head>
<meta charset="utf-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% trans "Central Authentication Service" %}{% endblock %}</title>
<link href="{{settings.CAS_COMPONENT_URLS.bootstrap3_css}}" rel="stylesheet">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="{{settings.CAS_COMPONENT_URLS.html5shiv}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.respond}}"></script>
<![endif]-->
{% if settings.CAS_FAVICON_URL %}<link rel="shortcut icon" href="{{settings.CAS_FAVICON_URL}}" />{% endif %}
<link href="{% static "cas_server/styles.css" %}" rel="stylesheet">
</head>
<body>
<div id="wrap">
<div class="container">
{% if auto_submit %}<noscript>{% endif %}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h1 id="app-name">
{% if settings.CAS_LOGO_URL %}<img src="{{settings.CAS_LOGO_URL}}" alt="cas-logo" />{% endif %}
Authentification Note Kfet 2020</h1>
</div>
</div>
{% if auto_submit %}</noscript>{% endif %}
<div class="row">
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% if auto_submit %}<noscript>{% endif %}
{% for msg in CAS_INFO_RENDER %}
<div class="alert alert-{{msg.type}}{% if msg.discardable %} alert-dismissable{% endif %}">
{% if msg.discardable %}<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="info-{{msg.name}}">&#215;</button>{% endif %}
<p>{{msg.message}}</p>
</div>
{% endfor %}
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">&#215;</button>
<p>{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}</p>
</div>
{% endif %}
{% block ante_messages %}{% endblock %}
{% for message in messages %}
<div {% spaceless %}
{% if message.level == message_levels.DEBUG %}
class="alert alert-warning"
{% elif message.level == message_levels.INFO %}
class="alert alert-info"
{% elif message.level == message_levels.SUCCESS %}
class="alert alert-success"
{% elif message.level == message_levels.WARNING %}
class="alert alert-warning"
{% else %}
class="alert alert-danger"
{% endif %}
{% endspaceless %}>
<p>{{message}}</p>
</div>
{% endfor %}
{% if auto_submit %}</noscript>{% endif %}
{% block content %}{% endblock %}
</div>
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-0"></div>
</div>
</div> <!-- /container -->
</div>
<div style="clear: both;"></div>
{% if settings.CAS_SHOW_POWERED %}
<div id="footer">
<p><a class="text-muted" href="https://pypi.org/project/django-cas-server/">django-cas-server powered</a></p>
</div>
{% endif %}
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
<script src="{% static "cas_server/functions.js" %}"></script>
<script type="text/javascript">
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
discard_and_remember("#alert-version", "cas-alert-version", "{{LAST_VERSION}}");
{% endif %}
{% for msg in CAS_INFO_RENDER %}
{% if msg.discardable %}
discard_and_remember("#info-{{msg.name}}", "cas-info-{{msg.name}}", "{{msg.hash}}");
{% endif %}
{% endfor %}
{% block javascript_inline %}{% endblock %}
</script>
{% block javascript %}{% endblock %}
</body>
</html>
<!--
Powered by django-cas-server version {{VERSION}}
Pypi: https://pypi.org/project/django-cas-server/
github: https://github.com/nitmir/django-cas-server
-->

View File

@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
{{ profile_form|crispy }} {{ profile_form|crispy }}
{{ soge_form|crispy }}
<button class="btn btn-success" type="submit"> <button class="btn btn-success" type="submit">
{% trans "Sign up" %} {% trans "Sign up" %}
</button> </button>

View File

@ -5,15 +5,14 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.urls import path, include from django.urls import path, include
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
from django.views.generic import RedirectView
from member.views import CustomLoginView from member.views import CustomLoginView
from .admin import admin_site from .admin import admin_site
from .views import IndexView
urlpatterns = [ urlpatterns = [
# Dev so redirect to something random # Dev so redirect to something random
path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), path('', IndexView.as_view(), name='index'),
# Include project routers # Include project routers
path('note/', include('note.urls')), path('note/', include('note.urls')),
@ -36,15 +35,15 @@ urlpatterns = [
path('coffee/', include('django_htcpcp_tea.urls')), path('coffee/', include('django_htcpcp_tea.urls')),
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # During development, serve media files
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if "oauth2_provider" in settings.INSTALLED_APPS:
if "cas_server" in settings.INSTALLED_APPS: # OAuth2 provider
urlpatterns += [ urlpatterns.append(
# Include CAS Server routers path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
path('cas/', include('cas_server.urls', namespace="cas_server")), )
]
if "debug_toolbar" in settings.INSTALLED_APPS: if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar import debug_toolbar

30
note_kfet/views.py Normal file
View File

@ -0,0 +1,30 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views.generic import RedirectView
from note.models import Alias
from permission.backends import PermissionBackend
class IndexView(LoginRequiredMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
"""
Calculate the index page according to the roles.
A normal user will have access to the transfer page.
A non-Kfet member will have access to its user detail page.
The user "note" will display the consumption interface.
"""
user = self.request.user
# The account note will have the consumption page as default page
if not PermissionBackend.check_perm(user, "auth.view_user", user):
return reverse("note:consos")
# People that can see the alias BDE are Kfet members
if PermissionBackend.check_perm(user, "alias.view_alias", Alias.objects.get(name="BDE")):
return reverse("note:transfer")
# Non-Kfet members will don't see the transfer page, but their profile page
return reverse("member:user_detail", args=(user.pk,))

View File

@ -1,17 +1,18 @@
beautifulsoup4~=4.7.1 beautifulsoup4~=4.7.1
Django~=2.2.15 Django~=2.2.15
django-bootstrap-datepicker-plus~=3.0.5 django-bootstrap-datepicker-plus~=3.0.5
django-cas-server>=1.2.0
django-colorfield~=0.3.2 django-colorfield~=0.3.2
django-crispy-forms~=1.7.2 django-crispy-forms~=1.7.2
django-extensions~=2.1.4 django-extensions~=2.1.4
django-filter~=2.1.0 django-filter~=2.1.0
django-htcpcp-tea~=0.3.1 django-htcpcp-tea~=0.3.1
django-mailer~=2.0.1 django-mailer~=2.0.1
django-oauth-toolkit~=1.1.2
django-phonenumber-field~=5.0.0 django-phonenumber-field~=5.0.0
django-polymorphic~=2.0.3 django-polymorphic~=2.0.3
djangorestframework~=3.9.0 djangorestframework~=3.9.0
django-rest-polymorphic~=0.1.9 django-rest-polymorphic~=0.1.9
django-tables2~=2.3.1 django-tables2~=2.3.1
python-memcached~=1.59
phonenumbers~=8.9.10 phonenumbers~=8.9.10
Pillow>=5.4.1 Pillow>=5.4.1