1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-03 22:32:51 +02:00

Compare commits

..

76 Commits

Author SHA1 Message Date
8434c0062c Merge branch 'beta' into JS_translations
# Conflicts:
#	apps/note/static/note/js/consos.js
#	locale/de/LC_MESSAGES/django.po
#	locale/es/LC_MESSAGES/django.po
#	locale/fr/LC_MESSAGES/django.po
2020-11-16 00:59:26 +01:00
6d976f32bf Update django oauth toolkit, fix #73 2020-11-16 00:49:53 +01:00
b9d49d53f2 Export JS translation files as static files 2020-11-16 00:29:27 +01:00
23243e09bb Fix some errors on JS string interpolation 2020-11-15 23:37:36 +01:00
2682e9a610 Add line in README on how to extract localized string in JS files 2020-11-15 23:31:10 +01:00
5635598bbc Extract strings from javascript files and translate them in french 2020-11-15 23:28:41 +01:00
b58a0c43cd Include auto-generated javascript translation file 2020-11-15 22:53:00 +01:00
7bd895c1df Grant treasurers to update a note picture 2020-10-26 17:58:30 +01:00
051591cb7a Don't see user detail in update form 2020-10-25 21:49:16 +01:00
0e7390b669 PC Kfet can see limited user information and clubs. It can create memberships but not see them 2020-10-25 21:38:04 +01:00
fe4363b83d Don't display too much detail when a user has no right to see a profile 2020-10-25 21:29:44 +01:00
6e80016b38 Don't delete object when checking an add permission: this is useless since we rollback to the initial DB state 2020-10-25 21:08:36 +01:00
08e50ffc22 Credit form didn't raise an error when the data didn't validate 2020-10-23 18:19:21 +02:00
224a0fdd8c SpecialTransactionProxy are force-saved 2020-10-23 16:55:33 +02:00
6dc7604e90 Alias were duplicated in profile alias list view 2020-10-23 16:48:33 +02:00
cb7f3c9f18 Note account can manage BDE memberships 2020-10-23 16:42:06 +02:00
f910feca9e PC Kfet can create and renew memberships 2020-10-23 13:17:07 +02:00
91f784872c Treasurers can update any roles, not only the BDE-related 2020-10-23 09:50:18 +02:00
58aa4983e3 The note account must be active in order to have access to the Rest Framework API 2020-10-20 10:30:41 +02:00
6cc3cf4174 A migration put the right role in the note account's memberships 2020-10-20 00:28:49 +02:00
2097e67321 Add permissions to PC Kfet 2020-10-20 00:19:49 +02:00
d773303d18 Add possibility to authenticate an account with its IP address 2020-10-19 23:44:56 +02:00
bf29efda0a Display real user name in the Soge credits list/detail 2020-10-08 10:36:30 +02:00
3eced33082 Well, everyone doesn't want a secondary bank account 2020-10-07 17:43:28 +02:00
acb3fb4a91 Highlight future users that declared that they opened a bank account 2020-10-07 17:42:46 +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
420a24ebac enable JavaScriptCatalog view 2020-09-19 22:42:35 +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
69 changed files with 2280 additions and 1185 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
``` ```
@ -267,14 +267,18 @@ La documentation plus haut niveau sur le développement est disponible sur [le W
### Regénérer les fichiers de traduction ### Regénérer les fichiers de traduction
Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`. Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv. Pour regénérer les traductions vous pouvez vous placer à la racine du projet et lancer le script `makemessages`.
Il faut penser à ignorer les dossiers ne contenant pas notre code, dont le virtualenv.
De plus, il faut aussi extraire les variables des fichiers JavaScript.
```bash ```bash
django-admin makemessages -i env python3 manage.py makemessages -i env
python3 manage.py makemessages -i env -e js -d djangojs
``` ```
Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec Une fois les fichiers édités, vous pouvez compiler les nouvelles traductions avec
```bash ```bash
django-admin compilemessages python3 manage.py compilemessages
python3 manage.py compilejsmessages
``` ```

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

@ -1,4 +1,10 @@
--- ---
- name: Collect static files
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Migrate Django database - name: Migrate Django database
command: /var/www/note_kfet/env/bin/python manage.py migrate command: /var/www/note_kfet/env/bin/python manage.py migrate
args: args:
@ -11,14 +17,14 @@
chdir: /var/www/note_kfet chdir: /var/www/note_kfet
become_user: www-data become_user: www-data
- name: Compile JavaScript messages
command: /var/www/note_kfet/env/bin/python manage.py compilejsmessages
args:
chdir: /var/www/note_kfet
become_user: www-data
- name: Install initial fixtures - name: Install initial fixtures
command: /var/www/note_kfet/env/bin/python manage.py loaddata initial command: /var/www/note_kfet/env/bin/python manage.py loaddata initial
args: args:
chdir: /var/www/note_kfet chdir: /var/www/note_kfet
become_user: postgres become_user: postgres
- name: Collect static files
command: /var/www/note_kfet/env/bin/python manage.py collectstatic --noinput
args:
chdir: /var/www/note_kfet
become_user: www-data

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
headers: {"X-CSRFTOKEN": CSRF_TOKEN} headers: {"X-CSRFTOKEN": CSRF_TOKEN}
}) })
.done(function() { .done(function() {
addMsg('Invité supprimé','success'); addMsg('{% trans "Guest deleted" %}', 'success');
$("#guests_table").load(location.pathname + " #guests_table"); $("#guests_table").load(location.pathname + " #guests_table");
}) })
.fail(function(xhr, textStatus, error) { .fail(function(xhr, textStatus, error) {

View File

@ -86,10 +86,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () { }).done(function () {
if (target.hasClass("table-info")) if (target.hasClass("table-info"))
addMsg( addMsg(
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", "{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000); "warning", 10000);
else else
addMsg("Entrée effectuée !", "success", 4000); addMsg("Entry made!", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function (xhr) { }).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000); errMsg(xhr.responseJSON, 4000);
@ -121,10 +121,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
}).done(function () { }).done(function () {
if (target.hasClass("table-info")) if (target.hasClass("table-info"))
addMsg( addMsg(
"Entrée effectuée, mais attention : la personne n'est plus adhérente Kfet.", "{% trans "Entry done, but caution: the user is not a Kfet member." %}",
"warning", 10000); "warning", 10000);
else else
addMsg("Entrée effectuée !", "success", 4000); addMsg("{% trans "Entry done!" %}", "success", 4000);
reloadTable(true); reloadTable(true);
}).fail(function (xhr) { }).fail(function (xhr) {
errMsg(xhr.responseJSON, 4000); errMsg(xhr.responseJSON, 4000);

View File

@ -12,8 +12,10 @@ 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
@ -288,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

@ -150,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/',
} }

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

@ -0,0 +1,50 @@
import sys
from django.db import migrations
def give_note_account_permissions(apps, schema_editor):
"""
Automatically manage the membership of the Note account.
"""
User = apps.get_model("auth", "user")
Membership = apps.get_model("member", "membership")
Role = apps.get_model("permission", "role")
note = User.objects.filter(username="note")
if not note.exists():
# We are in a test environment, don't log error message
if len(sys.argv) > 1 and sys.argv[1] == 'test':
return
print("Warning: Note account was not found. The note account was not imported.")
print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.")
print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you "
"don't want this account.")
return
note = note.get()
# Set for the two clubs a large expiration date and the correct role.
for m in Membership.objects.filter(user_id=note.id).all():
m.date_end = "3142-12-12"
m.roles.set(Role.objects.filter(name="PC Kfet").all())
m.save()
# By default, the note account is only authorized to be logged from localhost.
note.password = "ipbased$127.0.0.1"
note.is_active = True
note.save()
# Ensure that the note of the account is disabled
note.note.inactivity_reason = 'forced'
note.note.is_active = False
note.save()
class Migration(migrations.Migration):
dependencies = [
('member', '0005_remove_null_tag_on_charfields'),
('permission', '0001_initial'),
]
operations = [
migrations.RunPython(give_note_account_permissions),
]

View File

@ -14,7 +14,7 @@ function create_alias (e) {
}).done(function () { }).done(function () {
// Reload table // Reload table
$('#alias_table').load(location.pathname + ' #alias_table') $('#alias_table').load(location.pathname + ' #alias_table')
addMsg('Alias ajouté', 'success') addMsg(gettext('Alias successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) { }).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON) errMsg(xhr.responseJSON)
}) })
@ -22,7 +22,7 @@ function create_alias (e) {
/** /**
* On click of "delete", delete the alias * On click of "delete", delete the alias
* @param Integer button_id Alias id to remove * @param button_id:Integer Alias id to remove
*/ */
function delete_button (button_id) { function delete_button (button_id) {
$.ajax({ $.ajax({
@ -30,7 +30,7 @@ function delete_button (button_id) {
method: 'DELETE', method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN } headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () { }).done(function () {
addMsg('Alias supprimé', 'success') addMsg(gettext('Alias successfully deleted'), 'success')
$('#alias_table').load(location.pathname + ' #alias_table') $('#alias_table').load(location.pathname + ' #alias_table')
}).fail(function (xhr, _textStatus, _error) { }).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON) errMsg(xhr.responseJSON)

View File

@ -43,8 +43,24 @@ class UserTable(tables.Table):
section = tables.Column(accessor='profile__section') section = tables.Column(accessor='profile__section')
# Override the column to let replace the URL
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance")) balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail
# Replace also the URL
if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
value = ""
record.email = value
return value
def render_section(self, record, value):
return value \
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
else ""
def render_balance(self, record, value): def render_balance(self, record, value):
return pretty_money(value)\ return pretty_money(value)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "" if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else ""
@ -112,7 +128,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 %}
{% if club.name == "Kfet" %} {# Auto-renewal #}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }} The user is not a member of the club·s {{ clubs }}. An additional fee of {{ pretty_fee }}
will be charged to renew automatically the membership in this/these club·s. will be charged to renew automatically the membership in this/these club·s.
{% endblocktrans %} {% endblocktrans %}
{% else %} {% 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 %}
{% if club.name == "Kfet" %}
{% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %} {% blocktrans trimmed with clubs=clubs_renewal|join:", " pretty_fee=additional_fee_renewal|pretty_money %}
This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }} This club has parents {{ clubs }}. An additional fee of {{ pretty_fee }}
will be charged to adhere automatically to this/these club·s. will be charged to adhere automatically to this/these club·s.
{% endblocktrans %} {% 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

@ -25,6 +25,7 @@
</a> </a>
</dd> </dd>
{% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd> <dd class="col-xl-6">{{ user_object.profile.section }}</dd>
@ -45,6 +46,7 @@
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd> <dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %} {% endif %}
{% endif %}
</dl> </dl>
{% if user_object.pk == user.pk %} {% if user_object.pk == user.pk %}

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n perms %} {% load i18n perms %}
{% block content %} {% block content %}
{% if "member.change_profile_registration_valid"|has_perm:user %} {% if can_manage_registrations %}
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}"> <a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %} <i class="fa fa-user-plus"></i> {% trans "Registrations" %}
</a> </a>

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

@ -70,6 +70,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.") form.fields['email'].help_text = _("This address must be valid.")
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
context['profile_form'] = self.profile_form(instance=context['user_object'].profile, context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None) data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency: if not self.object.profile.report_frequency:
@ -157,8 +158,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
@ -166,6 +171,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\
@ -178,6 +185,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
@ -227,6 +235,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return qs return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
.filter(profile__registration_valid=False)
context["can_manage_registrations"] = pre_registered_users.exists()
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
@ -240,8 +255,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend context["aliases"] = AliasTable(
.filter_queryset(self.request.user, Alias, "view")).all()) note.alias_set.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -392,7 +407,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
@ -405,8 +421,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))
@ -438,8 +458,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend context["aliases"] = AliasTable(note.alias_set.filter(
.filter_queryset(self.request.user, Alias, "view")).all()) PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -610,6 +630,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,
@ -631,6 +654,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))
@ -645,11 +678,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name: if not last_name:
form.add_error('last_name', _("This field is required.")) form.add_error('last_name', _("This field is required."))
error = True
if not first_name: if not first_name:
form.add_error('first_name', _("This field is required.")) form.add_error('first_name', _("This field is required."))
error = True
if not bank and credit_type.special_type == "Chèque": if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required.")) form.add_error('bank', _("This field is required."))
return self.form_invalid(form) error = True
return not error return not error
@ -663,6 +698,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
@ -737,6 +773,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()
@ -774,7 +813,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

@ -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

@ -159,20 +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)}
@transaction.atomic
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))
@ -201,20 +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)}
@transaction.atomic
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))

View File

@ -217,6 +217,9 @@ 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
self.source = Note.objects.select_for_update().get(pk=self.source_id)
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
# Check that the amounts stay between big integer bounds # Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate() diff_source, diff_dest = self.validate()

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

@ -222,17 +222,14 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
if (!isNaN(source.balance)) { if (!isNaN(source.balance)) {
const newBalance = source.balance - quantity * amount const newBalance = source.balance - quantity * amount
if (newBalance <= -5000) { if (newBalance <= -5000) {
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'succès, mais la note émettrice ' + source_alias + ' est en négatif sévère.', 'but the emitter note %s is very negative.', [source_alias, source_alias])), 'danger', 30000)
'danger', 30000)
} else if (newBalance < 0) { } else if (newBalance < 0) {
addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'succès, mais la note émettrice ' + source_alias + ' est en négatif.', 'but the emitter note %s is negative.', [source_alias, source_alias])), 'warning', 30000)
'warning', 30000)
} }
if (source.membership && source.membership.date_end < new Date().toISOString()) { if (source.membership && source.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.', [source_alias])), 'danger', 30000)
'danger', 30000)
} }
} }
reset() reset()
@ -253,7 +250,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
template: template template: template
}).done(function () { }).done(function () {
reset() reset()
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000) addMsg(gettext("The transaction couldn't be validated because of insufficient balance."), 'danger', 10000)
}).fail(function () { }).fail(function () {
reset() reset()
errMsg(e.responseJSON) errMsg(e.responseJSON)

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) {
@ -235,20 +239,20 @@ $('#btn_transfer').click(function () {
if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) {
amount_field.addClass('is-invalid') amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>') $('#amount-required').html('<strong>' + gettext('This field is required and must contain a decimal positive number.') + '</strong>')
error = true error = true
} }
const amount = Math.floor(100 * amount_field.val()) const amount = Math.floor(100 * amount_field.val())
if (amount > 2147483647) { if (amount > 2147483647) {
amount_field.addClass('is-invalid') amount_field.addClass('is-invalid')
$('#amount-required').html('<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>') $('#amount-required').html('<strong>' + gettext('The amount must stay under 21,474,836.47 €.') + '</strong>')
error = true 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>' + gettext('This field is required.') + '</strong>')
error = true error = true
} }
@ -274,9 +278,8 @@ $('#btn_transfer').click(function () {
[...sources_notes_display].forEach(function (source) { [...sources_notes_display].forEach(function (source) {
[...dests_notes_display].forEach(function (dest) { [...dests_notes_display].forEach(function (dest) {
if (source.note.id === dest.note.id) { if (source.note.id === dest.note.id) {
addMsg('Attention : la transaction de ' + pretty_money(amount) + ' de la note ' + source.name + addMsg(interpolate(gettext('Warning: the transaction of %s from %s to %s was not made because ' +
' vers la note ' + dest.name + " n'a pas été faite car il s'agit de la même note au départ" + 'it is the same source and destination note.'), [pretty_money(amount), source.name, dest.name]), 'warning', 10000)
" et à l'arrivée.", 'warning', 10000)
LOCK = false LOCK = false
return return
} }
@ -296,43 +299,35 @@ $('#btn_transfer').click(function () {
destination_alias: dest.name destination_alias: dest.name
}).done(function () { }).done(function () {
if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
'danger', 30000)
} }
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) {
addMsg('Attention : la note destination ' + dest.name + " n'est plus adhérente.", addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
'danger', 30000)
} }
if (!isNaN(source.note.balance)) { if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -5000) { if (newBalance <= -5000) {
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
'mais la note émettrice est en négatif sévère.', 'danger', 10000)
reset() reset()
return return
} else if (newBalance < 0) { } else if (newBalance < 0) {
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is negative.'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' +
'mais la note émettrice est en négatif.', 'warning', 10000)
reset() reset()
return return
} }
} }
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Transfer of %s from %s to %s succeed!'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name]), 'success', 10000)
' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000)
reset() reset()
}).fail(function (err) { // do it again but valid = false }).fail(function (err) { // do it again but valid = false
const errObj = JSON.parse(err.responseText) const errObj = JSON.parse(err.responseText)
if (errObj.non_field_errors) { if (errObj.non_field_errors) {
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + [pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, errObj.non_field_errors]), 'danger')
' vers la note ' + dest.name + ' a échoué : ' + errObj.non_field_errors, 'danger')
LOCK = false LOCK = false
return return
} }
@ -352,17 +347,15 @@ $('#btn_transfer').click(function () {
destination: dest.note.id, destination: dest.note.id,
destination_alias: dest.name destination_alias: dest.name
}).done(function () { }).done(function () {
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, gettext('insufficient funds')]), 'danger', 10000)
' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000)
reset() reset()
}).fail(function (err) { }).fail(function (err) {
const errObj = JSON.parse(err.responseText) const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText } if (!error) { error = err.responseText }
addMsg('Le transfert de ' + addMsg(interpolate(gettext('Transfer of %s from %s to %s failed: %s'),
pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + [pretty_money(source.quantity * dest.quantity * amount), source.name, + dest.name, error]), 'danger')
' vers la note ' + dest.name + ' a échoué : ' + error, 'danger')
LOCK = false LOCK = false
}) })
}) })
@ -388,7 +381,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/',
@ -408,14 +401,14 @@ $('#btn_transfer').click(function () {
first_name: $('#first_name').val(), first_name: $('#first_name').val(),
bank: $('#bank').val() bank: $('#bank').val()
}).done(function () { }).done(function () {
addMsg('Le crédit/retrait a bien été effectué !', 'success', 10000) addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg('Attention : la note ' + alias + " n'est plus adhérente.", 'danger', 10000) } if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
reset() reset()
}).fail(function (err) { }).fail(function (err) {
const errObj = JSON.parse(err.responseText) const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText } if (!error) { error = err.responseText }
addMsg('Le crédit/retrait a échoué : ' + error, 'danger', 10000) addMsg(interpolate(gettext('Credit/debit failed: %s'), [error]), 'danger', 10000)
LOCK = false LOCK = false
}) })
} }

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"
} }
}, },
@ -799,12 +799,12 @@
"member", "member",
"membership" "membership"
], ],
"query": "{\"club\": [\"club\"]}", "query": "{}",
"type": "change", "type": "change",
"mask": 3, "mask": 3,
"field": "roles", "field": "roles",
"permanent": false, "permanent": false,
"description": "Modifier les rôles d'un adhérent d'un club" "description": "Modifier les rôles d'une adhésion"
} }
}, },
{ {
@ -2081,7 +2081,7 @@
], ],
"query": "{}", "query": "{}",
"type": "change", "type": "change",
"mask": 1, "mask": 2,
"field": "invalidity_reason", "field": "invalidity_reason",
"permanent": false, "permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction" "description": "Modifier la raison d'invalidité d'une transaction"
@ -2775,6 +2775,102 @@
"description": "Modifier n'importe quel profil non encore inscrit" "description": "Modifier n'importe quel profil non encore inscrit"
} }
}, },
{
"model": "permission.permission",
"pk": 178,
"fields": {
"model": [
"note",
"alias"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tous les alias, y compris ceux des non adhérents"
}
},
{
"model": "permission.permission",
"pk": 179,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__noteuser__user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"description": "Voir ses propres alias, pour toujours"
}
},
{
"model": "permission.permission",
"pk": 180,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"profile__registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 181,
"fields": {
"model": [
"member",
"profile"
],
"query": "{\"registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel profil non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 182,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur qui est adhérent BDE"
}
},
{
"model": "permission.permission",
"pk": 183,
"fields": {
"model": [
"note",
"note"
],
"query": "{}",
"type": "change",
"mask": 1,
"field": "display_image",
"permanent": false,
"description": "Changer l'image de n'importe quelle note"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -2845,7 +2941,8 @@
157, 157,
158, 158,
159, 159,
160 160,
179
] ]
} }
}, },
@ -2906,14 +3003,14 @@
62, 62,
127, 127,
133, 133,
135,
136, 136,
141, 141,
142, 142,
150, 150,
166, 166,
167, 167,
168 168,
182
] ]
} }
}, },
@ -2949,6 +3046,7 @@
31, 31,
32, 32,
33, 33,
51,
53, 53,
54, 54,
55, 55,
@ -2972,6 +3070,7 @@
137, 137,
138, 138,
139, 139,
140,
143, 143,
146, 146,
147, 147,
@ -2986,7 +3085,9 @@
174, 174,
175, 175,
176, 176,
177 177,
178,
183
] ]
} }
}, },
@ -3168,7 +3269,13 @@
174, 174,
175, 175,
176, 176,
177 177,
178,
179,
180,
181,
182,
183
] ]
} }
}, },
@ -3202,7 +3309,12 @@
170, 170,
171, 171,
176, 176,
177 177,
178,
179,
180,
181,
182
] ]
} }
}, },
@ -3365,7 +3477,6 @@
135, 135,
136, 136,
137, 137,
138,
139, 139,
140, 140,
143, 143,
@ -3378,6 +3489,41 @@
] ]
} }
}, },
{
"model": "permission.role",
"pk": 20,
"fields": {
"for_club": 2,
"name": "PC Kfet",
"permissions": [
6,
22,
24,
25,
26,
27,
30,
49,
50,
55,
56,
57,
58,
137,
143,
147,
150,
166,
167,
168,
176,
177,
180,
181,
182
]
}
},
{ {
"model": "wei.weirole", "model": "wei.weirole",
"pk": 12, "pk": 12,

View File

@ -43,7 +43,9 @@ 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._no_signal = True
o._force_delete = True o._force_delete = True
Model.delete(o) Model.delete(o)
# An object with pk 0 wouldn't deleted. That's not normal, we alert admins. # An object with pk 0 wouldn't deleted. That's not normal, we alert admins.
@ -61,9 +63,7 @@ class InstancedPermission:
obj._no_signal = True obj._no_signal = True
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 transaction.savepoint_rollback(sid)
obj._force_delete = True
Model.delete(obj)
return ret return ret

View File

@ -51,8 +51,10 @@ class ProtectQuerysetMixin:
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make # No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
# a custom request. # a custom request.
# We could also delete the field, but some views might be affected. # We could also delete the field, but some views might be affected.
meta = form.instance._meta
for key in form.base_fields: for key in form.base_fields:
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object): if not PermissionBackend.check_perm(self.request.user,
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
form.fields[key].widget = HiddenInput() form.fields[key].widget = HiddenInput()
return form return form

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"),

View File

@ -4,6 +4,8 @@
import django_tables2 as tables import django_tables2 as tables
from django.contrib.auth.models import User from django.contrib.auth.models import User
from treasury.models import SogeCredit
class FutureUserTable(tables.Table): class FutureUserTable(tables.Table):
""" """
@ -21,6 +23,7 @@ class FutureUserTable(tables.Table):
fields = ('last_name', 'first_name', 'username', 'email', ) fields = ('last_name', 'first_name', 'username', 'email', )
model = User model = User
row_attrs = { row_attrs = {
'class': 'table-row', 'class': lambda record: 'table-row'
+ (' bg-warning' if SogeCredit.objects.filter(user=record).exists() else ''),
'data-href': lambda record: record.pk 'data-href': lambda record: record.pk
} }

View File

@ -56,6 +56,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<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

@ -24,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
@ -42,6 +42,7 @@ 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"]
@ -72,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):
@ -182,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)
@ -227,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"] = SogeCredit.objects.filter(user=user).exists()
return ctx return ctx
def get_form(self, form_class=None): def get_form(self, form_class=None):
@ -307,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(
@ -373,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

@ -28,6 +28,8 @@ class TreasuryConfig(AppConfig):
source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy=None, specialtransactionproxy=None,
): ):
SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None) proxy = SpecialTransactionProxy(transaction=transaction, remittance=None)
proxy._force_save = True
proxy.save()
post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy) post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy)

View File

@ -10,7 +10,7 @@ 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):
@ -335,6 +335,11 @@ class SogeCredit(models.Model):
@transaction.atomic @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:
credit_transaction = SpecialTransaction( credit_transaction = SpecialTransaction(
source=NoteSpecial.objects.get(special_type="Virement bancaire"), source=NoteSpecial.objects.get(special_type="Virement bancaire"),

View File

@ -10,9 +10,8 @@ def save_special_transaction(instance, created, **kwargs):
""" """
if not hasattr(instance, "_no_signal"): if not hasattr(instance, "_no_signal"):
if instance.is_credit(): if created and RemittanceType.objects.filter(
if created and RemittanceType.objects.filter(note=instance.source).exists(): note=instance.source if instance.is_credit() else instance.destination).exists():
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save() proxy = SpecialTransactionProxy(transaction=instance, remittance=None)
else: proxy._force_save = True
if created and RemittanceType.objects.filter(note=instance.destination).exists(): proxy.save()
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()

View File

@ -147,4 +147,4 @@ class SogeCreditTable(tables.Table):
class Meta: class Meta:
model = SogeCredit model = SogeCredit
fields = ('user', 'amount', 'valid', ) fields = ('user', 'user__last_name', 'user__first_name', 'amount', 'valid', )

View File

@ -11,8 +11,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt> <dt class="col-xl-6 text-right">{% trans 'last name'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd> <dd class="col-xl-6">{{ object.user.last_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'first name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.first_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user.username }}</a></dd>
{% if "note.view_note_balance"|has_perm:object.user.note %} {% if "note.view_note_balance"|has_perm:object.user.note %}
<dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt> <dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt>

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

@ -431,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):

View File

@ -14,6 +14,7 @@ fi
# Set up Django project # Set up Django project
python3 manage.py collectstatic --noinput python3 manage.py collectstatic --noinput
python3 manage.py compilemessages python3 manage.py compilemessages
python3 manage.py compilejsmessages
python3 manage.py migrate python3 manage.py migrate
if [ "$1" ]; then if [ "$1" ]; then

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,119 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-15 23:21+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/member/static/member/js/alias.js:17
msgid "Alias successfully added"
msgstr ""
#: apps/member/static/member/js/alias.js:33
msgid "Alias successfully deleted"
msgstr ""
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is very negative."
msgstr ""
#: apps/note/static/note/js/consos.js:228
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is negative."
msgstr ""
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr ""
#: apps/note/static/note/js/consos.js:253
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
#: apps/note/static/note/js/transfer.js:238
msgid "This field is required and must contain a decimal positive number."
msgstr ""
#: apps/note/static/note/js/transfer.js:245
msgid "The amount must stay under 21,474,836.47 €."
msgstr ""
#: apps/note/static/note/js/transfer.js:251
msgid "This field is required."
msgstr ""
#: apps/note/static/note/js/transfer.js:277
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
"same source and destination note."
msgstr ""
#: apps/note/static/note/js/transfer.js:301
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr ""
#: apps/note/static/note/js/transfer.js:307
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is very negative."
msgstr ""
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is negative."
msgstr ""
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr ""
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr ""
#: apps/note/static/note/js/transfer.js:347
msgid "insufficient funds"
msgstr ""
#: apps/note/static/note/js/transfer.js:400
msgid "Credit/debit succeed!"
msgstr ""
#: apps/note/static/note/js/transfer.js:407
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr ""
#: note_kfet/static/js/base.js:366
msgid "An error occured while (in)validating this transaction:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,119 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-15 23:21+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/member/static/member/js/alias.js:17
msgid "Alias successfully added"
msgstr ""
#: apps/member/static/member/js/alias.js:33
msgid "Alias successfully deleted"
msgstr ""
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is very negative."
msgstr ""
#: apps/note/static/note/js/consos.js:228
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is negative."
msgstr ""
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr ""
#: apps/note/static/note/js/consos.js:253
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
#: apps/note/static/note/js/transfer.js:238
msgid "This field is required and must contain a decimal positive number."
msgstr ""
#: apps/note/static/note/js/transfer.js:245
msgid "The amount must stay under 21,474,836.47 €."
msgstr ""
#: apps/note/static/note/js/transfer.js:251
msgid "This field is required."
msgstr ""
#: apps/note/static/note/js/transfer.js:277
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
"same source and destination note."
msgstr ""
#: apps/note/static/note/js/transfer.js:301
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr ""
#: apps/note/static/note/js/transfer.js:307
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is very negative."
msgstr ""
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is negative."
msgstr ""
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr ""
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr ""
#: apps/note/static/note/js/transfer.js:347
msgid "insufficient funds"
msgstr ""
#: apps/note/static/note/js/transfer.js:400
msgid "Credit/debit succeed!"
msgstr ""
#: apps/note/static/note/js/transfer.js:407
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr ""
#: note_kfet/static/js/base.js:366
msgid "An error occured while (in)validating this transaction:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,134 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-15 23:21+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: apps/member/static/member/js/alias.js:17
msgid "Alias successfully added"
msgstr "Alias ajouté avec succès"
#: apps/member/static/member/js/alias.js:33
msgid "Alias successfully deleted"
msgstr "Alias supprimé avec succès"
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is very negative."
msgstr ""
"Attention, La transaction depuis la note %s a été réalisée avec succès, mais "
"la note émettrice %s est en négatif sévère."
#: apps/note/static/note/js/consos.js:228
#, javascript-format
msgid ""
"Warning, the transaction from the note %s succeed, but the emitter note %s "
"is negative."
msgstr ""
"Attention, La transaction depuis la note %s a été réalisée avec succès, mais "
"la note émettrice %s est en négatif."
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr "Attention, la note émettrice %s n'est plus adhérente."
#: apps/note/static/note/js/consos.js:253
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
"La transaction n'a pas pu être validée pour cause de solde insuffisant."
#: apps/note/static/note/js/transfer.js:238
msgid "This field is required and must contain a decimal positive number."
msgstr ""
"Ce champ est requis et doit comporter un nombre décimal strictement positif."
#: apps/note/static/note/js/transfer.js:245
msgid "The amount must stay under 21,474,836.47 €."
msgstr "Le montant ne doit pas excéder 21 474 836.47 €."
#: apps/note/static/note/js/transfer.js:251
msgid "This field is required."
msgstr "Ce champ est requis."
#: apps/note/static/note/js/transfer.js:277
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
"same source and destination note."
msgstr ""
"Attention : la transaction de %s de la note %s vers la note %s n'a pas été "
"faite car il s'agit de la même note au départ et à l'arrivée."
#: apps/note/static/note/js/transfer.js:301
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr "Attention, la note de destination %s n'est plus adhérente."
#: apps/note/static/note/js/transfer.js:307
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is very negative."
msgstr ""
"Attention, La transaction de %s depuis la note %s vers la note %s a été "
"réalisée avec succès, mais la note émettrice %s est en négatif sévère."
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
"the emitter note %s is negative."
msgstr ""
"Attention, La transaction de %s depuis la note %s vers la note %s a été "
"réalisée avec succès, mais la note émettrice %s est en négatif."
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr ""
"Le transfert de %s de la note %s vers la note %s a été fait avec succès !"
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr "Le transfert de %s de la note %s vers la note %s a échoué : %s"
#: apps/note/static/note/js/transfer.js:347
msgid "insufficient funds"
msgstr "solde insuffisant"
#: apps/note/static/note/js/transfer.js:400
msgid "Credit/debit succeed!"
msgstr "Le crédit/retrait a bien été effectué !"
#: apps/note/static/note/js/transfer.js:407
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr "Le crédit/retrait a échoué : %s"
#: note_kfet/static/js/base.js:366
msgid "An error occured while (in)validating this transaction:"
msgstr ""
"Une erreur est survenue lors de la validation/dévalidation de cette "
"transaction :"

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

@ -2,12 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
from django.contrib.auth import login
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser, User
from django.contrib.sessions.backends.db import SessionStore
from threading import local from threading import local
from django.contrib.sessions.backends.db import SessionStore
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
@ -78,6 +78,41 @@ class SessionMiddleware(object):
return response return response
class LoginByIPMiddleware(object):
"""
Allow some users to be authenticated based on their IP address.
For example, the "note" account should not be used elsewhere than the Kfet computer,
and should not have any password.
The password that is stored in database should be on the form "ipbased$my.public.ip.address".
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
"""
If the user is not authenticated, get the used IP address
and check if an user is authorized to be automatically logged with this address.
If it is the case, the logging is performed with the full rights.
"""
if not request.user.is_authenticated:
if 'HTTP_X_REAL_IP' in request.META:
ip = request.META.get('HTTP_X_REAL_IP')
elif 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
else:
ip = request.META.get('REMOTE_ADDR')
qs = User.objects.filter(password=f"ipbased${ip}")
if qs.exists():
login(request, qs.get())
session = request.session
session["permission_mask"] = 42
session.save()
return self.get_response(request)
class TurbolinksMiddleware(object): class TurbolinksMiddleware(object):
""" """
Send the `Turbolinks-Location` header in response to a visit that was redirected, Send the `Turbolinks-Location` header in response to a visit that was redirected,

View File

@ -49,16 +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:
MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',)
if DEBUG: if DEBUG:
PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor'] PASSWORD_HASHERS += ['member.hashers.DebugSuperuserBackdoor']
if "debug_toolbar" in INSTALLED_APPS: if "debug_toolbar" in INSTALLED_APPS:

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',
@ -77,6 +79,8 @@ MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware',
'django_htcpcp_tea.middleware.HTCPCPTeaMiddleware', 'django_htcpcp_tea.middleware.HTCPCPTeaMiddleware',
'note_kfet.middlewares.SessionMiddleware',
'note_kfet.middlewares.LoginByIPMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware', 'note_kfet.middlewares.TurbolinksMiddleware',
] ]
@ -214,6 +218,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': [
@ -233,7 +247,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

@ -363,8 +363,7 @@ function de_validate (id, validated, resourcetype) {
const errObj = JSON.parse(err.responseText) const errObj = JSON.parse(err.responseText)
let error = errObj.detail ? errObj.detail : errObj.non_field_errors let error = errObj.detail ? errObj.detail : errObj.non_field_errors
if (!error) { error = err.responseText } if (!error) { error = err.responseText }
addMsg('Une erreur est survenue lors de la validation/dévalidation ' + addMsg(gettext('An error occured while (in)validating this transaction:') + ' ' + error, 'danger')
'de cette transaction : ' + error, 'danger')
refreshBalance() refreshBalance()
// error if this method doesn't exist. Please define it. // error if this method doesn't exist. Please define it.

View File

@ -0,0 +1,134 @@
/*
* You should never see this file.
* It is here only for compatibility reasons in case of the command `compilejsmessages` was never executed.
* Please execute this command to generate translation strings.
*/
(function(globals) {
var django = globals.django || (globals.django = {});
django.pluralidx = function(n) {
var v=(n != 1);
if (typeof(v) == 'boolean') {
return v ? 1 : 0;
} else {
return v;
}
};
/* gettext library */
django.catalog = django.catalog || {};
if (!django.jsi18n_initialized) {
django.gettext = function(msgid) {
var value = django.catalog[msgid];
if (typeof(value) == 'undefined') {
return msgid;
} else {
return (typeof(value) == 'string') ? value : value[0];
}
};
django.ngettext = function(singular, plural, count) {
var value = django.catalog[singular];
if (typeof(value) == 'undefined') {
return (count == 1) ? singular : plural;
} else {
return value.constructor === Array ? value[django.pluralidx(count)] : value;
}
};
django.gettext_noop = function(msgid) { return msgid; };
django.pgettext = function(context, msgid) {
var value = django.gettext(context + '\x04' + msgid);
if (value.indexOf('\x04') != -1) {
value = msgid;
}
return value;
};
django.npgettext = function(context, singular, plural, count) {
var value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
if (value.indexOf('\x04') != -1) {
value = django.ngettext(singular, plural, count);
}
return value;
};
django.interpolate = function(fmt, obj, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
} else {
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
}
};
/* formatting library */
django.formats = {
"DATETIME_FORMAT": "j \\d\\e F \\d\\e Y \\a \\l\\a\\s H:i",
"DATETIME_INPUT_FORMATS": [
"%d/%m/%Y %H:%M:%S",
"%d/%m/%Y %H:%M:%S.%f",
"%d/%m/%Y %H:%M",
"%d/%m/%y %H:%M:%S",
"%d/%m/%y %H:%M:%S.%f",
"%d/%m/%y %H:%M",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%d %H:%M",
"%Y-%m-%d"
],
"DATE_FORMAT": "j \\d\\e F \\d\\e Y",
"DATE_INPUT_FORMATS": [
"%d/%m/%Y",
"%d/%m/%y",
"%Y-%m-%d"
],
"DECIMAL_SEPARATOR": ",",
"FIRST_DAY_OF_WEEK": 1,
"MONTH_DAY_FORMAT": "j \\d\\e F",
"NUMBER_GROUPING": 3,
"SHORT_DATETIME_FORMAT": "d/m/Y H:i",
"SHORT_DATE_FORMAT": "d/m/Y",
"THOUSAND_SEPARATOR": ".",
"TIME_FORMAT": "H:i",
"TIME_INPUT_FORMATS": [
"%H:%M:%S",
"%H:%M:%S.%f",
"%H:%M"
],
"YEAR_MONTH_FORMAT": "F \\d\\e Y"
};
django.get_format = function(format_type) {
var value = django.formats[format_type];
if (typeof(value) == 'undefined') {
return format_type;
} else {
return value;
}
};
/* add to global namespace */
globals.pluralidx = django.pluralidx;
globals.gettext = django.gettext;
globals.ngettext = django.ngettext;
globals.gettext_noop = django.gettext_noop;
globals.pgettext = django.pgettext;
globals.npgettext = django.npgettext;
globals.interpolate = django.interpolate;
globals.get_format = django.get_format;
django.jsi18n_initialized = true;
}
}(this));

View File

@ -0,0 +1 @@
_default.js

View File

@ -0,0 +1 @@
_default.js

View File

@ -0,0 +1 @@
_default.js

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 %}
@ -38,6 +38,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<script src="{% static "js/base.js" %}"></script> <script src="{% static "js/base.js" %}"></script>
<script src="{% static "js/konami.js" %}"></script> <script src="{% static "js/konami.js" %}"></script>
{# Translation in javascript files #}
<script src="{% static "js/jsi18n/jsi18n."|add:LANGUAGE_CODE|add:".js" %}"></script>
{# If extra ressources are needed for a form, load here #} {# If extra ressources are needed for a form, load here #}
{% if form.media %} {% if form.media %}
{{ form.media }} {{ form.media }}
@ -64,7 +67,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 +153,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">
{% if user.is_authenticated %}
{% if not user|is_member:"BDE" %}
<div class="alert alert-danger">
{% trans "You are not a BDE member anymore. Please renew your membership if you want to use the note." %}
</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"> <div class="alert alert-warning">
{% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %} {% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %}
</div> </div>
{% endif %} {% endif %}
<div id="messages"></div> {% 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 %}

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')),
@ -40,12 +39,11 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 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.3.3
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