diff --git a/.gitignore b/.gitignore index 94433be1f64d953e155d35a4daa08fcd90d98ca6..f541ab857d3b476bf6d86111b2c90606eece9f86 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,3 @@ backups/ env/ venv/ db.sqlite3 - -# Ignore migrations during first phase dev -migrations/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 80d85791e80c69942b59b3acda6275df29ad3b17..fc79afb623123d09f00acead76b2c240db7e179e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,10 @@ stages: - test - quality-assurance +# Also fetch submodules +variables: + GIT_SUBMODULE_STRATEGY: recursive + # Debian Buster py37-django22: stage: test @@ -15,7 +19,7 @@ py37-django22: python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-babel python3-lockfile python3-pip python3-phonenumbers python3-bs4 python3-setuptools tox - texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended + texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended script: tox -e py37-django22 # Ubuntu 20.04 @@ -33,7 +37,7 @@ py38-django22: python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil python3-babel python3-lockfile python3-pip python3-phonenumbers python3-bs4 python3-setuptools tox - texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended + texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended script: tox -e py38-django22 linters: diff --git a/Dockerfile b/Dockerfile index e2eb7d4aeef3fdfd2f272f8d47b82f6d4fb606b8..846cc420e414478f92c70c750616d5cbf75d08cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && \ python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \ python3-bs4 python3-setuptools \ uwsgi uwsgi-plugin-python3 \ - texlive-latex-base texlive-lang-french lmodern texlive-fonts-recommended \ + texlive-latex-base texlive-latex-recommended texlive-lang-french lmodern texlive-fonts-recommended \ gettext libjs-bootstrap4 fonts-font-awesome && \ rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 43c5a8a5d95cbaf3ed26316cd4ec6f11500072c9..a9eab62dd1588ba21d1dcf4266cc869d0ce398b6 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,6 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. DJANGO_SECRET_KEY=CHANGE_ME DJANGO_SETTINGS_MODULE="note_kfet.settings NOTE_URL=localhost # URL où accéder à la note - DOMAIN=localhost # note.example.com CONTACT_EMAIL=tresorerie.bde@localhost # Le reste n'est utile qu'en production, pour configurer l'envoi des mails NOTE_MAIL=notekfet@localhost @@ -211,7 +210,6 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous. $ source /env/bin/activate (env)$ ./manage.py check # pas de bêtise qui traine - (env)$ ./manage.py makemigrations (env)$ ./manage.py migrate 7. *Enjoy \o/* diff --git a/ansible/roles/1-apt-basic/tasks/main.yml b/ansible/roles/1-apt-basic/tasks/main.yml index fa51de34f387b7b7be96476d58cdda2108892d73..3148327f7aae41696e4dbbe2b889ea16952d9251 100644 --- a/ansible/roles/1-apt-basic/tasks/main.yml +++ b/ansible/roles/1-apt-basic/tasks/main.yml @@ -41,6 +41,7 @@ - texlive-fonts-recommended - texlive-lang-french - texlive-latex-base + - texlive-latex-recommended # WSGI server - uwsgi diff --git a/ansible/roles/7-postinstall/tasks/main.yml b/ansible/roles/7-postinstall/tasks/main.yml index 25fde0e77b64f787f549f137a743572c96af2e40..6f3017a53842916aee18e7c8664e36127a0d0b58 100644 --- a/ansible/roles/7-postinstall/tasks/main.yml +++ b/ansible/roles/7-postinstall/tasks/main.yml @@ -1,10 +1,4 @@ --- -- name: Make Django migrations - command: /var/www/note_kfet/env/bin/python manage.py makemigrations - args: - chdir: /var/www/note_kfet - become_user: www-data - - name: Migrate Django database command: /var/www/note_kfet/env/bin/python manage.py migrate args: diff --git a/apps/activity/migrations/0001_initial.py b/apps/activity/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..ba2d68dacadc7ea583d21a9422d1c4fa957fdcdc --- /dev/null +++ b/apps/activity/migrations/0001_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('description', models.TextField(verbose_name='description')), + ('location', models.CharField(blank=True, default='', help_text='Place where the activity is organized, eg. Kfet.', max_length=255, verbose_name='location')), + ('date_start', models.DateTimeField(verbose_name='start date')), + ('date_end', models.DateTimeField(verbose_name='end date')), + ('valid', models.BooleanField(default=False, verbose_name='valid')), + ('open', models.BooleanField(default=False, verbose_name='open')), + ], + options={ + 'verbose_name': 'activity', + 'verbose_name_plural': 'activities', + }, + ), + migrations.CreateModel( + name='ActivityType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('manage_entries', models.BooleanField(default=False, help_text='Enable the support of entries for this activity.', verbose_name='manage entries')), + ('can_invite', models.BooleanField(default=False, verbose_name='can invite')), + ('guest_entry_fee', models.PositiveIntegerField(default=0, verbose_name='guest entry fee')), + ], + options={ + 'verbose_name': 'activity type', + 'verbose_name_plural': 'activity types', + }, + ), + migrations.CreateModel( + name='Entry', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='entry time')), + ], + options={ + 'verbose_name': 'entry', + 'verbose_name_plural': 'entries', + }, + ), + migrations.CreateModel( + name='Guest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last_name', models.CharField(max_length=255, verbose_name='last name')), + ('first_name', models.CharField(max_length=255, verbose_name='first name')), + ], + options={ + 'verbose_name': 'guest', + 'verbose_name_plural': 'guests', + }, + ), + ] diff --git a/apps/activity/migrations/0002_auto_20200904_2341.py b/apps/activity/migrations/0002_auto_20200904_2341.py new file mode 100644 index 0000000000000000000000000000000000000000..4afa74dd5c8bad9b32e5a4b1e84d01a28dd45988 --- /dev/null +++ b/apps/activity/migrations/0002_auto_20200904_2341.py @@ -0,0 +1,89 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('activity', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('member', '0001_initial'), + ('note', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='GuestTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Transaction')), + ('entry', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to='activity.Entry')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('note.transaction',), + ), + migrations.AddField( + model_name='guest', + name='activity', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='activity.Activity'), + ), + migrations.AddField( + model_name='guest', + name='inviter', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='guests', to='note.NoteUser', verbose_name='inviter'), + ), + migrations.AddField( + model_name='entry', + name='activity', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='activity.Activity', verbose_name='activity'), + ), + migrations.AddField( + model_name='entry', + name='guest', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.PROTECT, to='activity.Guest'), + ), + migrations.AddField( + model_name='entry', + name='note', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.NoteUser', verbose_name='note'), + ), + migrations.AddField( + model_name='activity', + name='activity_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='activity.ActivityType', verbose_name='type'), + ), + migrations.AddField( + model_name='activity', + name='attendees_club', + field=models.ForeignKey(help_text='Club that is authorized to join the activity. Mostly the Kfet club.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='attendees club'), + ), + migrations.AddField( + model_name='activity', + name='creater', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + migrations.AddField( + model_name='activity', + name='organizer', + field=models.ForeignKey(help_text='Club that organizes the activity. The entry fees will go to this club.', on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='organizer'), + ), + migrations.AlterUniqueTogether( + name='guest', + unique_together={('activity', 'last_name', 'first_name')}, + ), + migrations.AlterUniqueTogether( + name='entry', + unique_together={('activity', 'note', 'guest')}, + ), + migrations.AlterUniqueTogether( + name='activity', + unique_together={('name', 'date_start', 'date_end')}, + ), + ] diff --git a/apps/activity/models.py b/apps/activity/models.py index 0aefaf59884e1639ae0c34973fe7bc2da98ffaf9..9d3431be18dd1eac49684630c2300b5488fe31cf 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,6 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import os from datetime import timedelta from threading import Thread @@ -133,9 +134,13 @@ class Activity(models.Model): if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS: def refresh_activities(): from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand - RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name) - RefreshActivitiesCommand.refresh_raw_wiki_page("Modification de l'activité " + self.name) - Thread(daemon=True, target=refresh_activities).start() + # Consider that we can update the wiki iff the WIKI_PASSWORD env var is not empty + RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name, + False, os.getenv("WIKI_PASSWORD")) + RefreshActivitiesCommand.refresh_raw_wiki_page("Modification de l'activité " + self.name, + False, os.getenv("WIKI_PASSWORD")) + Thread(daemon=True, target=refresh_activities).start()\ + if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities() return ret def __str__(self): diff --git a/apps/logs/migrations/0001_initial.py b/apps/logs/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..e517edfb1945c593fda675edd3a9122eebac5f82 --- /dev/null +++ b/apps/logs/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Changelog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address')), + ('instance_pk', models.CharField(max_length=255, verbose_name='identifier')), + ('previous', models.TextField(null=True, verbose_name='previous data')), + ('data', models.TextField(null=True, verbose_name='new data')), + ('action', models.CharField(choices=[('create', 'create'), ('edit', 'edit'), ('delete', 'delete')], default='edit', max_length=16, verbose_name='action')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='timestamp')), + ('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType', verbose_name='model')), + ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'changelog', + 'verbose_name_plural': 'changelogs', + }, + ), + ] diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json deleted file mode 100644 index e27eb72d65a27dd10ca0b0faf6ea37bb73bed7ec..0000000000000000000000000000000000000000 --- a/apps/member/fixtures/initial.json +++ /dev/null @@ -1,31 +0,0 @@ -[ - { - "model": "member.club", - "pk": 1, - "fields": { - "name": "BDE", - "email": "tresorerie.bde@example.com", - "require_memberships": true, - "membership_fee_paid": 500, - "membership_fee_unpaid": 500, - "membership_duration": 396, - "membership_start": "2019-08-31", - "membership_end": "2020-09-30" - } - }, - { - "model": "member.club", - "pk": 2, - "fields": { - "name": "Kfet", - "email": "tresorerie.bde@example.com", - "parent_club": 1, - "require_memberships": true, - "membership_fee_paid": 3500, - "membership_fee_unpaid": 3500, - "membership_duration": 396, - "membership_start": "2019-08-31", - "membership_end": "2020-09-30" - } - } -] diff --git a/apps/member/migrations/0001_initial.py b/apps/member/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..be357568f2d64282d3805383fa179a283f89e8bf --- /dev/null +++ b/apps/member/migrations/0001_initial.py @@ -0,0 +1,76 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Club', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), + ('email', models.EmailField(max_length=254, verbose_name='email')), + ('require_memberships', models.BooleanField(default=True, help_text="Uncheck if this club don't require memberships.", verbose_name='require memberships')), + ('membership_fee_paid', models.PositiveIntegerField(default=0, verbose_name='membership fee (paid students)')), + ('membership_fee_unpaid', models.PositiveIntegerField(default=0, verbose_name='membership fee (unpaid students)')), + ('membership_duration', models.PositiveIntegerField(blank=True, help_text='The longest time (in days) a membership can last (NULL = infinite).', null=True, verbose_name='membership duration')), + ('membership_start', models.DateField(blank=True, help_text='Date from which the members can renew their membership.', null=True, verbose_name='membership start')), + ('membership_end', models.DateField(blank=True, help_text='Maximal date of a membership, after which members must renew it.', null=True, verbose_name='membership end')), + ], + options={ + 'verbose_name': 'club', + 'verbose_name_plural': 'clubs', + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=50, null=True, region=None, verbose_name='phone number')), + ('section', models.CharField(blank=True, help_text='e.g. "1A0", "9A♥", "SAPHIRE"', max_length=255, null=True, verbose_name='section')), + ('department', models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ('A2', "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department')), + ('promotion', models.PositiveSmallIntegerField(default=2020, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion')), + ('address', models.CharField(blank=True, max_length=255, null=True, verbose_name='address')), + ('paid', models.BooleanField(default=False, help_text='Tells if the user receive a salary.', verbose_name='paid')), + ('ml_events_registration', models.CharField(blank=True, choices=[(None, 'No'), ('fr', 'Yes (receive them in french)'), ('en', 'Yes (receive them in english)')], default=None, max_length=2, null=True, verbose_name='Register on the mailing list to stay informed of the events of the campus (1 mail/week)')), + ('ml_sport_registration', models.BooleanField(default=False, verbose_name='Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)')), + ('ml_art_registration', models.BooleanField(default=False, verbose_name='Register on the mailing list to stay informed of the art events of the campus (1 mail/week)')), + ('report_frequency', models.PositiveSmallIntegerField(default=0, verbose_name='report frequency (in days)')), + ('last_report', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last report date')), + ('email_confirmed', models.BooleanField(default=False, verbose_name='email confirmed')), + ('registration_valid', models.BooleanField(default=False, verbose_name='registration valid')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'user profile', + 'verbose_name_plural': 'user profile', + }, + ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_start', models.DateField(default=datetime.date.today, verbose_name='membership starts on')), + ('date_end', models.DateField(null=True, verbose_name='membership ends on')), + ('fee', models.PositiveIntegerField(verbose_name='fee')), + ('club', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='club')), + ], + options={ + 'verbose_name': 'membership', + 'verbose_name_plural': 'memberships', + }, + ), + ] diff --git a/apps/member/migrations/0002_auto_20200904_2341.py b/apps/member/migrations/0002_auto_20200904_2341.py new file mode 100644 index 0000000000000000000000000000000000000000..3dd9d0456abc89dd0a048ff5a4d1e8584d46bbd2 --- /dev/null +++ b/apps/member/migrations/0002_auto_20200904_2341.py @@ -0,0 +1,42 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('permission', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('member', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='membership', + name='roles', + field=models.ManyToManyField(to='permission.Role', verbose_name='roles'), + ), + migrations.AddField( + model_name='membership', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + migrations.AddField( + model_name='club', + name='parent_club', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='parent club'), + ), + migrations.AddIndex( + model_name='profile', + index=models.Index(fields=['user'], name='member_prof_user_id_30c316_idx'), + ), + migrations.AddIndex( + model_name='membership', + index=models.Index(fields=['user'], name='member_memb_user_id_945dbc_idx'), + ), + ] diff --git a/apps/member/migrations/0003_create_bde_and_kfet.py b/apps/member/migrations/0003_create_bde_and_kfet.py new file mode 100644 index 0000000000000000000000000000000000000000..0464569fff929045e23ab30aa91c27ebe33c0fa7 --- /dev/null +++ b/apps/member/migrations/0003_create_bde_and_kfet.py @@ -0,0 +1,57 @@ +from django.db import migrations + + +def create_bde_and_kfet(apps, schema_editor): + """ + The clubs BDE and Kfet are pre-injected. + """ + Club = apps.get_model("member", "club") + NoteClub = apps.get_model("note", "noteclub") + ContentType = apps.get_model('contenttypes', 'ContentType') + polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id + + Club.objects.get_or_create( + id=1, + name="BDE", + email="tresorerie.bde@example.com", + require_memberships=True, + membership_fee_paid=500, + membership_fee_unpaid=500, + membership_duration=396, + membership_start="2020-08-01", + membership_end="2021-09-30", + ) + Club.objects.get_or_create( + id=2, + name="Kfet", + parent_club_id=1, + email="tresorerie.bde@example.com", + require_memberships=True, + membership_fee_paid=500, + membership_fee_unpaid=500, + membership_duration=396, + membership_start="2020-08-01", + membership_end="2021-09-30", + ) + + NoteClub.objects.get_or_create( + id=5, + club_id=1, + polymorphic_ctype_id=polymorphic_ctype_id, + ) + NoteClub.objects.get_or_create( + id=6, + club_id=2, + polymorphic_ctype_id=polymorphic_ctype_id, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ('member', '0002_auto_20200904_2341'), + ('note', '0002_create_special_notes'), + ] + + operations = [ + migrations.RunPython(create_bde_and_kfet), + ] diff --git a/apps/member/static/member/js/alias.js b/apps/member/static/member/js/alias.js index 2d652dde5ef232fb0282e912e8e519d595d866c8..444e1bcff70f4d91a5f6e09ecb3b75e9aca67d54 100644 --- a/apps/member/static/member/js/alias.js +++ b/apps/member/static/member/js/alias.js @@ -2,22 +2,22 @@ * On form submit, create a new alias */ function create_alias (e) { - // Do not submit HTML form - e.preventDefault(); + // Do not submit HTML form + e.preventDefault() - // Get data and send to API - const formData = new FormData(e.target); - $.post("/api/note/alias/", { - "csrfmiddlewaretoken": formData.get("csrfmiddlewaretoken"), - "name": formData.get("name"), - "note": formData.get("note") - }).done(function () { - // Reload table - $("#alias_table").load(location.pathname + " #alias_table"); - addMsg("Alias ajouté", "success"); - }).fail(function (xhr, _textStatus, _error) { - errMsg(xhr.responseJSON); - }); + // Get data and send to API + const formData = new FormData(e.target) + $.post('/api/note/alias/', { + csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'), + name: formData.get('name'), + note: formData.get('note') + }).done(function () { + // Reload table + $('#alias_table').load(location.pathname + ' #alias_table') + addMsg('Alias ajouté', 'success') + }).fail(function (xhr, _textStatus, _error) { + errMsg(xhr.responseJSON) + }) } /** @@ -25,19 +25,19 @@ function create_alias (e) { * @param Integer button_id Alias id to remove */ function delete_button (button_id) { - $.ajax({ - url: "/api/note/alias/" + button_id + "/", - method: "DELETE", - headers: { "X-CSRFTOKEN": CSRF_TOKEN } - }).done(function () { - addMsg('Alias supprimé', 'success'); - $("#alias_table").load(location.pathname + " #alias_table"); - }).fail(function (xhr, _textStatus, _error) { - errMsg(xhr.responseJSON); - }); + $.ajax({ + url: '/api/note/alias/' + button_id + '/', + method: 'DELETE', + headers: { 'X-CSRFTOKEN': CSRF_TOKEN } + }).done(function () { + addMsg('Alias supprimé', 'success') + $('#alias_table').load(location.pathname + ' #alias_table') + }).fail(function (xhr, _textStatus, _error) { + errMsg(xhr.responseJSON) + }) } $(document).ready(function () { - // Attach event - document.getElementById("form_alias").addEventListener("submit", create_alias); -}) \ No newline at end of file + // Attach event + document.getElementById('form_alias').addEventListener('submit', create_alias) +}) diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py index ef8b820992e4cefb01371df2ccfe181908918855..90b1f3828d19e34751fdc1382e84c8bcb159a2b1 100644 --- a/apps/member/tests/test_memberships.py +++ b/apps/member/tests/test_memberships.py @@ -197,7 +197,7 @@ class TestMemberships(TestCase): # Create a new membership response = self.client.post(reverse("member:club_add_member", args=(club.pk,)), data=dict( user=user.pk, - date_start="{:%Y-%m-%d}".format(timezone.now().date()), + date_start="{:%Y-%m-%d}".format(date.today()), soge=False, credit_type=NoteSpecial.objects.get(special_type="Espèces").id, credit_amount=4200, @@ -236,7 +236,7 @@ class TestMemberships(TestCase): # Renew membership response = self.client.post(reverse("member:club_renew_membership", args=(membership.pk,)), data=dict( user=user.pk, - date_start="{:%Y-%m-%d}".format(timezone.now().date()), + date_start="{:%Y-%m-%d}".format(date.today()), soge=bde_parent, credit_type=NoteSpecial.objects.get(special_type="Chèque").id, credit_amount=14242, @@ -265,7 +265,7 @@ class TestMemberships(TestCase): response = self.client.post(reverse("member:club_add_member", args=(bde.pk,)), data=dict( user=user.pk, - date_start="{:%Y-%m-%d}".format(timezone.now().date()), + date_start="{:%Y-%m-%d}".format(date.today()), soge=True, credit_type=NoteSpecial.objects.get(special_type="Virement bancaire").id, credit_amount=(bde.membership_fee_paid + kfet.membership_fee_paid) / 100, diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json deleted file mode 100644 index 72853eb76acf3128bafc26c34b1b1c28da70afde..0000000000000000000000000000000000000000 --- a/apps/note/fixtures/initial.json +++ /dev/null @@ -1,188 +0,0 @@ -[ - { - "model": "note.note", - "pk": 1, - "fields": { - "polymorphic_ctype": [ - "note", - "notespecial" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "", - "created_at": "2020-02-20T20:02:48.778Z" - } - }, - { - "model": "note.note", - "pk": 2, - "fields": { - "polymorphic_ctype": [ - "note", - "notespecial" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "", - "created_at": "2020-02-20T20:06:39.546Z" - } - }, - { - "model": "note.note", - "pk": 3, - "fields": { - "polymorphic_ctype": [ - "note", - "notespecial" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "", - "created_at": "2020-02-20T20:06:43.049Z" - } - }, - { - "model": "note.note", - "pk": 4, - "fields": { - "polymorphic_ctype": [ - "note", - "notespecial" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "", - "created_at": "2020-02-20T20:06:50.996Z" - } - }, - { - "model": "note.note", - "pk": 5, - "fields": { - "polymorphic_ctype": [ - "note", - "noteclub" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "pic/default.png", - "created_at": "2020-02-20T20:09:38.615Z" - } - }, - { - "model": "note.note", - "pk": 6, - "fields": { - "polymorphic_ctype": [ - "note", - "noteclub" - ], - "balance": 0, - "last_negative": null, - "is_active": true, - "display_image": "pic/default.png", - "created_at": "2020-02-20T20:16:14.753Z" - } - }, - { - "model": "note.noteclub", - "pk": 5, - "fields": { - "club": 1 - } - }, - { - "model": "note.noteclub", - "pk": 6, - "fields": { - "club": 2 - } - }, - { - "model": "note.notespecial", - "pk": 1, - "fields": { - "special_type": "Esp\u00e8ces" - } - }, - { - "model": "note.notespecial", - "pk": 2, - "fields": { - "special_type": "Carte bancaire" - } - }, - { - "model": "note.notespecial", - "pk": 3, - "fields": { - "special_type": "Ch\u00e8que" - } - }, - { - "model": "note.notespecial", - "pk": 4, - "fields": { - "special_type": "Virement bancaire" - } - }, - { - "model": "note.alias", - "pk": 1, - "fields": { - "name": "Esp\u00e8ces", - "normalized_name": "especes", - "note": 1 - } - }, - { - "model": "note.alias", - "pk": 2, - "fields": { - "name": "Carte bancaire", - "normalized_name": "cartebancaire", - "note": 2 - } - }, - { - "model": "note.alias", - "pk": 3, - "fields": { - "name": "Ch\u00e8que", - "normalized_name": "cheque", - "note": 3 - } - }, - { - "model": "note.alias", - "pk": 4, - "fields": { - "name": "Virement bancaire", - "normalized_name": "virementbancaire", - "note": 4 - } - }, - { - "model": "note.alias", - "pk": 5, - "fields": { - "name": "BDE", - "normalized_name": "bde", - "note": 5 - } - }, - { - "model": "note.alias", - "pk": 6, - "fields": { - "name": "Kfet", - "normalized_name": "kfet", - "note": 6 - } - } -] \ No newline at end of file diff --git a/apps/note/migrations/0001_initial.py b/apps/note/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..a0ebda316ff938e4e01df9afd7b1e8fefde54cb8 --- /dev/null +++ b/apps/note/migrations/0001_initial.py @@ -0,0 +1,209 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('member', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('balance', models.BigIntegerField(default=0, help_text='in centimes, money credited for this instance', verbose_name='account balance')), + ('last_negative', models.DateTimeField(blank=True, help_text='last time the balance was negative', null=True, verbose_name='last negative date')), + ('display_image', models.ImageField(default='pic/default.png', max_length=255, upload_to='pic/', verbose_name='display image')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created at')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this note should be treated as active. Unselect this instead of deleting notes.', verbose_name='active')), + ('inactivity_reason', models.CharField(choices=[('manual', 'The user blocked his/her note manually, eg. when he/she left the school for holidays. It can be reactivated at any time.'), ('forced', "The note is blocked by the the BDE and can't be manually reactivated.")], default=None, max_length=255, null=True)), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_note.note_set+', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'note', + 'verbose_name_plural': 'notes', + }, + ), + migrations.CreateModel( + name='TemplateCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=31, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'transaction category', + 'verbose_name_plural': 'transaction categories', + }, + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('source_alias', models.CharField(default='', max_length=255, verbose_name='used alias')), + ('destination_alias', models.CharField(default='', max_length=255, verbose_name='used alias')), + ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created at')), + ('quantity', models.PositiveIntegerField(default=1, verbose_name='quantity')), + ('amount', models.PositiveIntegerField(verbose_name='amount')), + ('reason', models.CharField(max_length=255, verbose_name='reason')), + ('valid', models.BooleanField(default=True, verbose_name='valid')), + ('invalidity_reason', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='invalidity reason')), + ('destination', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='note.Note', verbose_name='destination')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_note.transaction_set+', to='contenttypes.ContentType')), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='note.Note', verbose_name='source')), + ], + options={ + 'verbose_name': 'transaction', + 'verbose_name_plural': 'transactions', + }, + ), + migrations.CreateModel( + name='MembershipTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Transaction')), + ], + options={ + 'verbose_name': 'membership transaction', + 'verbose_name_plural': 'membership transactions', + }, + bases=('note.transaction',), + ), + migrations.CreateModel( + name='NoteClub', + fields=[ + ('note_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Note')), + ], + options={ + 'verbose_name': 'club note', + 'verbose_name_plural': 'clubs notes', + }, + bases=('note.note',), + ), + migrations.CreateModel( + name='NoteSpecial', + fields=[ + ('note_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Note')), + ('special_type', models.CharField(max_length=255, unique=True, verbose_name='type')), + ], + options={ + 'verbose_name': 'special note', + 'verbose_name_plural': 'special notes', + }, + bases=('note.note',), + ), + migrations.CreateModel( + name='NoteUser', + fields=[ + ('note_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Note')), + ], + options={ + 'verbose_name': "one's note", + 'verbose_name_plural': 'users note', + }, + bases=('note.note',), + ), + migrations.CreateModel( + name='RecurrentTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Transaction')), + ], + options={ + 'verbose_name': 'recurrent transaction', + 'verbose_name_plural': 'recurrent transactions', + }, + bases=('note.transaction',), + ), + migrations.CreateModel( + name='SpecialTransaction', + fields=[ + ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.Transaction')), + ('last_name', models.CharField(max_length=255, verbose_name='name')), + ('first_name', models.CharField(max_length=255, verbose_name='first_name')), + ('bank', models.CharField(blank=True, max_length=255, verbose_name='bank')), + ], + options={ + 'verbose_name': 'Special transaction', + 'verbose_name_plural': 'Special transactions', + }, + bases=('note.transaction',), + ), + migrations.CreateModel( + name='Alias', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), + ('normalized_name', models.CharField(editable=False, max_length=255, unique=True)), + ('note', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.Note')), + ], + options={ + 'verbose_name': 'alias', + 'verbose_name_plural': 'aliases', + }, + ), + migrations.CreateModel( + name='TransactionTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(error_messages={'unique': 'A template with this name already exist'}, max_length=255, unique=True, verbose_name='name')), + ('amount', models.PositiveIntegerField(verbose_name='amount')), + ('display', models.BooleanField(default=True, verbose_name='display')), + ('highlighted', models.BooleanField(default=False, verbose_name='highlighted')), + ('description', models.CharField(blank=True, max_length=255, verbose_name='description')), + ('category', models.ForeignKey(max_length=31, on_delete=django.db.models.deletion.PROTECT, related_name='templates', to='note.TemplateCategory', verbose_name='type')), + ('destination', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='note.NoteClub', verbose_name='destination')), + ], + options={ + 'verbose_name': 'transaction template', + 'verbose_name_plural': 'transaction templates', + }, + ), + migrations.AddIndex( + model_name='transaction', + index=models.Index(fields=['created_at'], name='note_transa_created_bea8b1_idx'), + ), + migrations.AddIndex( + model_name='transaction', + index=models.Index(fields=['source'], name='note_transa_source__4a1a1e_idx'), + ), + migrations.AddIndex( + model_name='transaction', + index=models.Index(fields=['destination'], name='note_transa_destina_6e1bb4_idx'), + ), + migrations.AddField( + model_name='recurrenttransaction', + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='note.TransactionTemplate'), + ), + migrations.AddField( + model_name='noteuser', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to=settings.AUTH_USER_MODEL, verbose_name='user'), + ), + migrations.AddField( + model_name='noteclub', + name='club', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='note', to='member.Club', verbose_name='club'), + ), + migrations.AddField( + model_name='membershiptransaction', + name='membership', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='member.Membership'), + ), + migrations.AddIndex( + model_name='alias', + index=models.Index(fields=['name'], name='note_alias_name_a89405_idx'), + ), + migrations.AddIndex( + model_name='alias', + index=models.Index(fields=['normalized_name'], name='note_alias_normali_bd52b4_idx'), + ), + ] diff --git a/apps/note/migrations/0002_create_special_notes.py b/apps/note/migrations/0002_create_special_notes.py new file mode 100644 index 0000000000000000000000000000000000000000..12fa8583bcab2a82e0f4717358ccca1160fb62f6 --- /dev/null +++ b/apps/note/migrations/0002_create_special_notes.py @@ -0,0 +1,25 @@ +from django.db import migrations + + +def create_special_notes(apps, schema_editor): + """ + We create the four special note to make transfers. + """ + NoteSpecial = apps.get_model("note", "notespecial") + ContentType = apps.get_model('contenttypes', 'ContentType') + polymorphic_ctype_id = ContentType.objects.get_for_model(NoteSpecial).id + + NoteSpecial.objects.get_or_create(id=1, special_type="Espèces", polymorphic_ctype_id=polymorphic_ctype_id) + NoteSpecial.objects.get_or_create(id=2, special_type="Carte bancaire", polymorphic_ctype_id=polymorphic_ctype_id) + NoteSpecial.objects.get_or_create(id=3, special_type="Chèque", polymorphic_ctype_id=polymorphic_ctype_id) + NoteSpecial.objects.get_or_create(id=4, special_type="Virement bancaire", polymorphic_ctype_id=polymorphic_ctype_id) + + +class Migration(migrations.Migration): + dependencies = [ + ('note', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_special_notes), + ] diff --git a/apps/permission/migrations/0001_initial.py b/apps/permission/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..c82aa0324b4a2eac80fa56d6e880d71685e024d2 --- /dev/null +++ b/apps/permission/migrations/0001_initial.py @@ -0,0 +1,71 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('member', '0001_initial'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Permission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('query', models.TextField(verbose_name='query')), + ('type', models.CharField(choices=[('add', 'add'), ('view', 'view'), ('change', 'change'), ('delete', 'delete')], max_length=15, verbose_name='type')), + ('field', models.CharField(blank=True, max_length=255, verbose_name='field')), + ('permanent', models.BooleanField(default=False, help_text='Tells if the permission should be granted even if the membership of the user is expired.', verbose_name='permanent')), + ('description', models.CharField(blank=True, max_length=255, verbose_name='description')), + ], + options={ + 'verbose_name': 'permission', + 'verbose_name_plural': 'permissions', + }, + ), + migrations.CreateModel( + name='PermissionMask', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rank', models.PositiveSmallIntegerField(unique=True, verbose_name='rank')), + ('description', models.CharField(max_length=255, unique=True, verbose_name='description')), + ], + options={ + 'verbose_name': 'permission mask', + 'verbose_name_plural': 'permission masks', + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('for_club', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club')), + ('permissions', models.ManyToManyField(to='permission.Permission', verbose_name='permissions')), + ], + options={ + 'verbose_name': 'role permissions', + 'verbose_name_plural': 'role permissions', + }, + ), + migrations.AddField( + model_name='permission', + name='mask', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='permissions', to='permission.PermissionMask', verbose_name='mask'), + ), + migrations.AddField( + model_name='permission', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.ContentType', verbose_name='model'), + ), + migrations.AlterUniqueTogether( + name='permission', + unique_together={('model', 'query', 'type', 'field')}, + ), + ] diff --git a/apps/scripts b/apps/scripts index 525f091b0caddc69cb2da7eba545ab9609bb1bb0..7479671b3f6c499958d3ad991eb54de3010f4da8 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit 525f091b0caddc69cb2da7eba545ab9609bb1bb0 +Subproject commit 7479671b3f6c499958d3ad991eb54de3010f4da8 diff --git a/apps/treasury/migrations/0001_initial.py b/apps/treasury/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..0231d2935f91ee5605a31b244e814ff5f330f7b4 --- /dev/null +++ b/apps/treasury/migrations/0001_initial.py @@ -0,0 +1,107 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('note', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.PositiveIntegerField(primary_key=True, serialize=False, verbose_name='Invoice identifier')), + ('bde', models.CharField(choices=[('Saperlistpopette.png', 'Saper[list]popette'), ('Finalist.png', 'Fina[list]'), ('Listorique.png', '[List]orique'), ('Satellist.png', 'Satel[list]'), ('Monopolist.png', 'Monopo[list]'), ('Kataclist.png', 'Katac[list]')], default='Saperlistpopette.png', max_length=32, verbose_name='BDE')), + ('object', models.CharField(max_length=255, verbose_name='Object')), + ('description', models.TextField(verbose_name='Description')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('address', models.TextField(verbose_name='Address')), + ('date', models.DateField(default=datetime.date.today, verbose_name='Date')), + ('acquitted', models.BooleanField(default=False, verbose_name='Acquitted')), + ('locked', models.BooleanField(default=False, help_text="An invoice can't be edited when it is locked.", verbose_name='Locked')), + ('tex', models.TextField(default='', verbose_name='tex source')), + ], + options={ + 'verbose_name': 'invoice', + 'verbose_name_plural': 'invoices', + }, + ), + migrations.CreateModel( + name='Remittance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date')), + ('comment', models.CharField(max_length=255, verbose_name='Comment')), + ('closed', models.BooleanField(default=False, verbose_name='Closed')), + ], + options={ + 'verbose_name': 'remittance', + 'verbose_name_plural': 'remittances', + }, + ), + migrations.CreateModel( + name='SpecialTransactionProxy', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('remittance', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='treasury.Remittance', verbose_name='Remittance')), + ('transaction', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='note.SpecialTransaction')), + ], + options={ + 'verbose_name': 'special transaction proxy', + 'verbose_name_plural': 'special transaction proxies', + }, + ), + migrations.CreateModel( + name='SogeCredit', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('credit_transaction', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='note.SpecialTransaction', verbose_name='credit transaction')), + ('transactions', models.ManyToManyField(related_name='_sogecredit_transactions_+', to='note.MembershipTransaction', verbose_name='membership transactions')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'Credit from the Société générale', + 'verbose_name_plural': 'Credits from the Société générale', + }, + ), + migrations.CreateModel( + name='RemittanceType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('note', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='note.NoteSpecial')), + ], + options={ + 'verbose_name': 'remittance type', + 'verbose_name_plural': 'remittance types', + }, + ), + migrations.AddField( + model_name='remittance', + name='remittance_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='treasury.RemittanceType', verbose_name='Type'), + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('designation', models.CharField(max_length=255, verbose_name='Designation')), + ('quantity', models.PositiveIntegerField(verbose_name='Quantity')), + ('amount', models.IntegerField(verbose_name='Unit price')), + ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='treasury.Invoice', verbose_name='invoice')), + ], + options={ + 'verbose_name': 'product', + 'verbose_name_plural': 'products', + }, + ), + ] diff --git a/apps/treasury/templates/treasury/invoice_sample.tex b/apps/treasury/templates/treasury/invoice_sample.tex index d7ec7391f99a29fbbdf4f2d00734c2e8398486c2..c9bb92b892acaf558d8c2559e1e634dcf5ba88f7 100644 --- a/apps/treasury/templates/treasury/invoice_sample.tex +++ b/apps/treasury/templates/treasury/invoice_sample.tex @@ -7,13 +7,15 @@ \usepackage[T1]{fontenc} \usepackage[utf8]{inputenc} \usepackage[a4paper]{geometry} -\usepackage{units} -\usepackage{bera} +%\usepackage{bera} \usepackage{graphicx} \usepackage{fancyhdr} \usepackage{fp} \usepackage{transparent} \usepackage{eso-pic} +\usepackage{ifthen} + +\DeclareUnicodeCharacter{00B0}{$^\circ$} \def\TVA{0} % Taux de la TVA diff --git a/apps/treasury/tests/test_treasury.py b/apps/treasury/tests/test_treasury.py index 15d35cb3fc0ba1099d1ed717edbb92fc7c03150f..343b91ec8eb80e72e02e39a81d253062565b5986 100644 --- a/apps/treasury/tests/test_treasury.py +++ b/apps/treasury/tests/test_treasury.py @@ -1,5 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from unittest import skip from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -143,6 +144,7 @@ class TestInvoices(TestCase): self.assertRedirects(response, reverse("treasury:invoice_list"), 302, 200) self.assertFalse(Invoice.objects.filter(pk=self.invoice.id).exists()) + @skip("LaTeX is buggy in the CI") def test_invoice_render_pdf(self): """ Generate the PDF file of an invoice. diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 5889f8b5330c9cb4b95767417e71f27be16b2d1f..2bddf319d862b824db5383f56ca0becab9f79566 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -217,7 +217,9 @@ class InvoiceRenderView(LoginRequiredMixin, View): ).wait() if error: - raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")") + with open("{}/invoice-{:d}.log".format(tmp_dir, pk), "r") as f: + log = f.read() + raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")\n\n" + log) # Display the generated pdf as a HTTP Response pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() diff --git a/apps/wei/migrations/0001_initial.py b/apps/wei/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..0ad73fab9fda4c6bbd4c8355f6dd25a938628ce2 --- /dev/null +++ b/apps/wei/migrations/0001_initial.py @@ -0,0 +1,120 @@ +# Generated by Django 2.2.16 on 2020-09-04 21:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('permission', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('member', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Bus', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('description', models.TextField(blank=True, default='', verbose_name='description')), + ('information_json', models.TextField(default='{}', help_text='Information about the survey for new members, encoded in JSON', verbose_name='survey information')), + ], + options={ + 'verbose_name': 'Bus', + 'verbose_name_plural': 'Buses', + }, + ), + migrations.CreateModel( + name='BusTeam', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('color', models.PositiveIntegerField(help_text='The color of the T-Shirt, stored with its number equivalent', verbose_name='color')), + ('description', models.TextField(blank=True, default='', verbose_name='description')), + ('bus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams', to='wei.Bus', verbose_name='bus')), + ], + options={ + 'verbose_name': 'Bus team', + 'verbose_name_plural': 'Bus teams', + 'unique_together': {('bus', 'name')}, + }, + ), + migrations.CreateModel( + name='WEIClub', + fields=[ + ('club_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.Club')), + ('year', models.PositiveIntegerField(default=2020, unique=True, verbose_name='year')), + ('date_start', models.DateField(verbose_name='date start')), + ('date_end', models.DateField(verbose_name='date end')), + ], + options={ + 'verbose_name': 'WEI', + 'verbose_name_plural': 'WEI', + }, + bases=('member.club',), + ), + migrations.CreateModel( + name='WEIRole', + fields=[ + ('role_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='permission.Role')), + ], + options={ + 'verbose_name': 'WEI Role', + 'verbose_name_plural': 'WEI Roles', + }, + bases=('permission.role',), + ), + migrations.CreateModel( + name='WEIRegistration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('soge_credit', models.BooleanField(default=False, verbose_name='Credit from Société générale')), + ('caution_check', models.BooleanField(default=False, verbose_name='Caution check given')), + ('birth_date', models.DateField(verbose_name='birth date')), + ('gender', models.CharField(choices=[('male', 'Male'), ('female', 'Female'), ('nonbinary', 'Non binary')], max_length=16, verbose_name='gender')), + ('clothing_cut', models.CharField(choices=[('male', 'Male'), ('female', 'Female')], max_length=16, verbose_name='clothing cut')), + ('clothing_size', models.CharField(choices=[('XS', 'XS'), ('S', 'S'), ('M', 'M'), ('L', 'L'), ('XL', 'XL'), ('XXL', 'XXL')], max_length=4, verbose_name='clothing size')), + ('health_issues', models.TextField(blank=True, default='', verbose_name='health issues')), + ('emergency_contact_name', models.CharField(max_length=255, verbose_name='emergency contact name')), + ('emergency_contact_phone', phonenumber_field.modelfields.PhoneNumberField(max_length=32, region=None, verbose_name='emergency contact phone')), + ('first_year', models.BooleanField(default=False, help_text='Tells if the user is new in the school.', verbose_name='first year')), + ('information_json', models.TextField(default='{}', help_text='Information about the registration (buses for old members, survey fot the new members), encoded in JSON', verbose_name='registration information')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wei', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ('wei', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='users', to='wei.WEIClub', verbose_name='WEI')), + ], + options={ + 'verbose_name': 'WEI User', + 'verbose_name_plural': 'WEI Users', + 'unique_together': {('user', 'wei')}, + }, + ), + migrations.CreateModel( + name='WEIMembership', + fields=[ + ('membership_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='member.Membership')), + ('bus', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='wei.Bus', verbose_name='bus')), + ('registration', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='membership', to='wei.WEIRegistration', verbose_name='WEI registration')), + ('team', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='wei.BusTeam', verbose_name='team')), + ], + options={ + 'verbose_name': 'WEI membership', + 'verbose_name_plural': 'WEI memberships', + }, + bases=('member.membership',), + ), + migrations.AddField( + model_name='bus', + name='wei', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='buses', to='wei.WEIClub', verbose_name='WEI'), + ), + migrations.AlterUniqueTogether( + name='bus', + unique_together={('wei', 'name')}, + ), + ] diff --git a/apps/wei/views.py b/apps/wei/views.py index 358cb14a436ace69d4c8def136cf6977013ee417..7720829ce7255e044b2bef548034c31f1c9ca667 100644 --- a/apps/wei/views.py +++ b/apps/wei/views.py @@ -1110,7 +1110,9 @@ class MemberListRenderView(LoginRequiredMixin, View): ).wait() if error: - raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")") + with open("{}/wei-list.log".format(tmp_dir), "r") as f: + log = f.read() + raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")\n\n" + log) # Display the generated pdf as a HTTP Response with open("{}/wei-list.pdf".format(tmp_dir), 'rb') as f: diff --git a/entrypoint.sh b/entrypoint.sh index ab33fbe81ce6143db18017b2aa12bcbbab8d03ff..dfd68977d9c5c8643062e88786d652ad77687143 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -14,7 +14,6 @@ fi # Set up Django project python3 manage.py collectstatic --noinput python3 manage.py compilemessages -python3 manage.py makemigrations python3 manage.py migrate if [ "$1" ]; then diff --git a/note_kfet/static/js/autocomplete_model.js b/note_kfet/static/js/autocomplete_model.js index c4f9405be5c6f54b0c97bc27e127272290d33650..f7aafbc6c1c8195b0451263533611619a1d36e97 100644 --- a/note_kfet/static/js/autocomplete_model.js +++ b/note_kfet/static/js/autocomplete_model.js @@ -1,57 +1,53 @@ $(document).ready(function () { - $(".autocomplete").keyup(function(e) { - let target = $("#" + e.target.id); - let prefix = target.attr("id"); - let api_url = target.attr("api_url"); - let api_url_suffix = target.attr("api_url_suffix"); - if (!api_url_suffix) - api_url_suffix = ""; - let name_field = target.attr("name_field"); - if (!name_field) - name_field = "name"; - let input = target.val(); - target.addClass("is-invalid"); - target.removeClass("is-valid"); - $("#" + prefix + "_reset").removeClass("d-none"); - - $.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) { - let html = ""; - - objects.results.forEach(function (obj) { - html += li(prefix + "_" + obj.id, obj[name_field]); - }); - - let results_list = $("#" + prefix + "_list"); - results_list.html(html); - - objects.results.forEach(function (obj) { - $("#" + prefix + "_" + obj.id).click(function() { - target.val(obj[name_field]); - $("#" + prefix + "_pk").val(obj.id); - - results_list.html(""); - target.removeClass("is-invalid"); - target.addClass("is-valid"); - - if (typeof autocompleted != 'undefined') - autocompleted(obj, prefix) - }); - - if (input === obj[name_field]) - $("#" + prefix + "_pk").val(obj.id); - }); - - if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) { - results_list.children().first().trigger("click"); - } - }); - }); - - $(".autocomplete-reset").click(function() { - let name = $(this).attr("id").replace("_reset", ""); - $("#" + name + "_pk").val(""); - $("#" + name).val(""); - $("#" + name + "_list").html(""); - $(this).addClass("d-none"); - }); -}); \ No newline at end of file + $('.autocomplete').keyup(function (e) { + const target = $('#' + e.target.id) + const prefix = target.attr('id') + const api_url = target.attr('api_url') + let api_url_suffix = target.attr('api_url_suffix') + if (!api_url_suffix) { api_url_suffix = '' } + let name_field = target.attr('name_field') + if (!name_field) { name_field = 'name' } + const input = target.val() + target.addClass('is-invalid') + target.removeClass('is-valid') + $('#' + prefix + '_reset').removeClass('d-none') + + $.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) { + let html = '' + + objects.results.forEach(function (obj) { + html += li(prefix + '_' + obj.id, obj[name_field]) + }) + + const results_list = $('#' + prefix + '_list') + results_list.html(html) + + objects.results.forEach(function (obj) { + $('#' + prefix + '_' + obj.id).click(function () { + target.val(obj[name_field]) + $('#' + prefix + '_pk').val(obj.id) + + results_list.html('') + target.removeClass('is-invalid') + target.addClass('is-valid') + + if (typeof autocompleted !== 'undefined') { autocompleted(obj, prefix) } + }) + + if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) } + }) + + if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) { + results_list.children().first().trigger('click') + } + }) + }) + + $('.autocomplete-reset').click(function () { + const name = $(this).attr('id').replace('_reset', '') + $('#' + name + '_pk').val('') + $('#' + name).val('') + $('#' + name + '_list').html('') + $(this).addClass('d-none') + }) +}) diff --git a/note_kfet/static/js/base.js b/note_kfet/static/js/base.js index 8315f01a58387f2cfcf954b503735f7daeb74de8..87a99f64f5bed5df5f52a72de11a092b91260676 100644 --- a/note_kfet/static/js/base.js +++ b/note_kfet/static/js/base.js @@ -1,18 +1,16 @@ // Copyright (C) 2018-2020 by BDE ENS Paris-Saclay // SPDX-License-Identifier: GPL-3.0-or-later - /** * Convert balance in cents to a human readable amount * @param value the balance, in cents * @returns {string} */ function pretty_money (value) { - if (value % 100 === 0) - return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €"; - else - return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "." - + (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €"; + if (value % 100 === 0) { return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + ' €' } else { + return (value < 0 ? '- ' : '') + Math.floor(Math.abs(value) / 100) + '.' + + (Math.abs(value) % 100 < 10 ? '0' : '') + (Math.abs(value) % 100) + ' €' + } } /** @@ -22,19 +20,19 @@ function pretty_money (value) { * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. */ function addMsg (msg, alert_type, timeout = -1) { - let msgDiv = $("#messages"); - let html = msgDiv.html(); - let id = Math.floor(10000 * Math.random() + 1); - html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" + - "<button id=\"close-message-" + id + "\" class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>" - + msg + "</div>\n"; - msgDiv.html(html); - - if (timeout > 0) { - setTimeout(function () { - $("#close-message-" + id).click(); - }, timeout); - } + const msgDiv = $('#messages') + let html = msgDiv.html() + const id = Math.floor(10000 * Math.random() + 1) + html += '<div class="alert alert-' + alert_type + ' alert-dismissible">' + + '<button id="close-message-' + id + '" class="close" data-dismiss="alert" href="#"><span aria-hidden="true">×</span></button>' + + msg + '</div>\n' + msgDiv.html(html) + + if (timeout > 0) { + setTimeout(function () { + $('#close-message-' + id).click() + }, timeout) + } } /** @@ -43,34 +41,34 @@ function addMsg (msg, alert_type, timeout = -1) { * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. */ function errMsg (errs_obj, timeout = -1) { - for (const err_msg of Object.values(errs_obj)) { - addMsg(err_msg, 'danger', timeout); - } + for (const err_msg of Object.values(errs_obj)) { + addMsg(err_msg, 'danger', timeout) + } } var reloadWithTurbolinks = (function () { - var scrollPosition; + var scrollPosition - function reload () { - scrollPosition = [window.scrollX, window.scrollY]; - Turbolinks.visit(window.location.toString(), { action: 'replace' }) - } + function reload () { + scrollPosition = [window.scrollX, window.scrollY] + Turbolinks.visit(window.location.toString(), { action: 'replace' }) + } - document.addEventListener('turbolinks:load', function () { - if (scrollPosition) { - window.scrollTo.apply(window, scrollPosition); - scrollPosition = null - } - }); + document.addEventListener('turbolinks:load', function () { + if (scrollPosition) { + window.scrollTo.apply(window, scrollPosition) + scrollPosition = null + } + }) - return reload; -})(); + return reload +})() /** * Reload the balance of the user on the right top corner */ function refreshBalance () { - $("#user_balance").load("/ #user_balance"); + $('#user_balance').load('/ #user_balance') } /** @@ -79,15 +77,15 @@ function refreshBalance () { * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. */ function getMatchedNotes (pattern, fun) { - $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club", fun); + $.getJSON('/api/note/alias/?format=json&alias=' + pattern + '&search=user|club', fun) } /** * Generate a <li> entry with a given id and text */ function li (id, text, extra_css) { - return "<li class=\"list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate " - + (extra_css ? extra_css : "") + "\"" + " id=\"" + id + "\">" + text + "</li>\n"; + return '<li class="list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate ' + + (extra_css || '') + '"' + ' id="' + id + '">' + text + '</li>\n' } /** @@ -95,24 +93,13 @@ function li (id, text, extra_css) { * @param note The concerned note. */ function displayStyle (note) { - if (!note) - return ""; - let balance = note.balance; - var css = ""; - if (balance < -5000) - css += " text-danger bg-dark"; - else if (balance < -1000) - css += " text-danger"; - else if (balance < 0) - css += " text-warning"; - else if (!note.email_confirmed) - css += " text-white bg-primary"; - else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) - css += "text-white bg-info"; - return css; + if (!note) { return '' } + const balance = note.balance + var css = '' + if (balance < -5000) { css += ' text-danger bg-dark' } else if (balance < -1000) { css += ' text-danger' } else if (balance < 0) { css += ' text-warning' } else if (!note.email_confirmed) { css += ' text-white bg-primary' } else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += 'text-white bg-info' } + return css } - /** * Render note name and picture * @param note The note to render @@ -121,23 +108,22 @@ function displayStyle (note) { * @param profile_pic_field */ function displayNote (note, alias, user_note_field = null, profile_pic_field = null) { - if (!note.display_image) { - note.display_image = '/static/member/img/default_picture.png'; - } - let img = note.display_image; - if (alias !== note.name && note.name) - alias += " (aka. " + note.name + ")"; - if (user_note_field !== null) { - $("#" + user_note_field).removeAttr('class'); - $("#" + user_note_field).addClass(displayStyle(note)); - $("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance)))); - if (profile_pic_field != null) { - $("#" + profile_pic_field).attr('src', img); - $("#" + profile_pic_field + "_link").attr('href', note.resourcetype === "NoteUser" ? - "/accounts/user/" + note.user : note.resourcetype === "NoteClub" ? - "/accounts/club/" + note.club : "#"); - } + if (!note.display_image) { + note.display_image = '/static/member/img/default_picture.png' + } + const img = note.display_image + if (alias !== note.name && note.name) { alias += ' (aka. ' + note.name + ')' } + if (user_note_field !== null) { + $('#' + user_note_field).removeAttr('class') + $('#' + user_note_field).addClass(displayStyle(note)) + $('#' + user_note_field).text(alias + (note.balance == null ? '' : (' :\n' + pretty_money(note.balance)))) + if (profile_pic_field != null) { + $('#' + profile_pic_field).attr('src', img) + $('#' + profile_pic_field + '_link').attr('href', note.resourcetype === 'NoteUser' + ? '/accounts/user/' + note.user : note.resourcetype === 'NoteClub' + ? '/accounts/club/' + note.club : '#') } + } } /** @@ -152,35 +138,34 @@ function displayNote (note, alias, user_note_field = null, profile_pic_field = n * (useful in consumptions, put null if not used) * @returns an anonymous function to be compatible with jQuery events */ -function removeNote (d, note_prefix = "note", notes_display, note_list_id, user_note_field = null, profile_pic_field = null) { - return (function () { - let new_notes_display = []; - let html = ""; - notes_display.forEach(function (disp) { - if (disp.quantity > 1 || disp.id !== d.id) { - disp.quantity -= disp.id === d.id ? 1 : 0; - new_notes_display.push(disp); - html += li(note_prefix + "_" + disp.id, disp.name - + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>", - displayStyle(disp.note)); - } - }); - - notes_display.length = 0; - new_notes_display.forEach(function (disp) { - notes_display.push(disp); - }); - - $("#" + note_list_id).html(html); - notes_display.forEach(function (disp) { - let obj = $("#" + note_prefix + "_" + disp.id); - obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); - obj.hover(function () { - if (disp.note) - displayNote(disp.note, disp.name, user_note_field, profile_pic_field); - }); - }); - }); +function removeNote (d, note_prefix = 'note', notes_display, note_list_id, user_note_field = null, profile_pic_field = null) { + return function () { + const new_notes_display = [] + let html = '' + notes_display.forEach(function (disp) { + if (disp.quantity > 1 || disp.id !== d.id) { + disp.quantity -= disp.id === d.id ? 1 : 0 + new_notes_display.push(disp) + html += li(note_prefix + '_' + disp.id, disp.name + + '<span class="badge badge-dark badge-pill">' + disp.quantity + '</span>', + displayStyle(disp.note)) + } + }) + + notes_display.length = 0 + new_notes_display.forEach(function (disp) { + notes_display.push(disp) + }) + + $('#' + note_list_id).html(html) + notes_display.forEach(function (disp) { + const obj = $('#' + note_prefix + '_' + disp.id) + obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)) + obj.hover(function () { + if (disp.note) { displayNote(disp.note, disp.name, user_note_field, profile_pic_field) } + }) + }) + } } /** @@ -199,203 +184,193 @@ function removeNote (d, note_prefix = "note", notes_display, note_list_id, user_ * the associated note is not displayed. * Useful for a consumption if the item is selected before. */ -function autoCompleteNote (field_id, note_list_id, notes, notes_display, alias_prefix = "alias", - note_prefix = "note", user_note_field = null, profile_pic_field = null, alias_click = null) { - let field = $("#" + field_id); - - // Configure tooltip - field.tooltip({ - html: true, - placement: 'bottom', - title: 'Loading...', - trigger: 'manual', - container: field.parent(), - fallbackPlacement: 'clockwise' - }); - - // When the user clicks elsewhere, we hide the tooltip - $(document).click(function(e) { - if (!e.target.id.startsWith(alias_prefix)) { - field.tooltip("hide"); - } - }); - - let old_pattern = null; - - // Clear search on click - field.click(function () { - field.tooltip('hide'); - field.removeClass('is-invalid'); - field.val(""); - old_pattern = ""; - }); - - // When the user type "Enter", the first alias is clicked - field.keypress(function (event) { - if (event.originalEvent.charCode === 13 && notes.length > 0) { - let li_obj = field.parent().find("ul li").first(); - displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); - li_obj.trigger("click"); - } - }); - - // When the user type something, the matched aliases are refreshed - field.keyup(function (e) { - field.removeClass('is-invalid'); - - if (e.originalEvent.charCode === 13) - return; - - let pattern = field.val(); - - // If the pattern is not modified, we don't query the API - if (pattern === old_pattern) - return; - old_pattern = pattern; - notes.length = 0; - - // get matched Alias with note associated - if (pattern === "") { - field.tooltip('hide'); - notes.length = 0; - return; - } - - $.getJSON("/api/note/consumer/?format=json&alias=" + pattern + "&search=user|club", - function (consumers) { - // The response arrived too late, we stop the request - if (pattern !== $("#" + field_id).val()) - return; - - // Build tooltip content - let aliases_matched_html = '<ul class="list-group list-group-flush">'; - consumers.results.forEach(function (consumer) { - let note = consumer.note; - note.email_confirmed = consumer.email_confirmed; - if (consumer.hasOwnProperty("membership") && consumer.membership) - note.membership = consumer.membership; - else - note.membership = undefined; - let extra_css = displayStyle(note); - aliases_matched_html += li(alias_prefix + '_' + consumer.id, - consumer.name, - extra_css); - notes.push(note); - }); - aliases_matched_html += '</ul>'; - - // Show tooltip - field.attr('data-original-title', aliases_matched_html).tooltip('show'); - - consumers.results.forEach(function (consumer) { - let consumer_obj = $("#" + alias_prefix + "_" + consumer.id); - consumer_obj.hover(function () { - displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field) - }); - consumer_obj.click(function () { - var disp = null; - notes_display.forEach(function (d) { - // We compare the alias ids - if (d.id === consumer.id) { - d.quantity += 1; - disp = d; - } - }); - // In the other case, we add a new emitter - if (disp == null) { - disp = { - name: consumer.name, - id: consumer.id, - note: consumer.note, - quantity: 1 - }; - notes_display.push(disp); - } - - // If the function alias_click exists, it is called. If it doesn't return true, then the notes are - // note displayed. Useful for a consumption when a button is already clicked - if (alias_click && !alias_click()) - return; - - let note_list = $("#" + note_list_id); - let html = ""; - notes_display.forEach(function (disp) { - html += li(note_prefix + "_" + disp.id, - disp.name - + "<span class=\"badge badge-dark badge-pill\">" - + disp.quantity + "</span>", - displayStyle(disp.note)); - }); - - // Emitters are displayed - note_list.html(html); - - // Update tooltip position - field.tooltip('update'); - - notes_display.forEach(function (disp) { - let line_obj = $("#" + note_prefix + "_" + disp.id); - // Hover an emitter display also the profile picture - line_obj.hover(function () { - displayNote(disp.note, disp.name, user_note_field, profile_pic_field); - }); - - // When an emitter is clicked, it is removed - line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, - profile_pic_field)); - }); - }) - }); - - });// end getJSON alias - }); -}// end function autocomplete +function autoCompleteNote (field_id, note_list_id, notes, notes_display, alias_prefix = 'alias', + note_prefix = 'note', user_note_field = null, profile_pic_field = null, alias_click = null) { + const field = $('#' + field_id) + + // Configure tooltip + field.tooltip({ + html: true, + placement: 'bottom', + title: 'Loading...', + trigger: 'manual', + container: field.parent(), + fallbackPlacement: 'clockwise' + }) + + // When the user clicks elsewhere, we hide the tooltip + $(document).click(function (e) { + if (!e.target.id.startsWith(alias_prefix)) { + field.tooltip('hide') + } + }) + + let old_pattern = null + + // Clear search on click + field.click(function () { + field.tooltip('hide') + field.removeClass('is-invalid') + field.val('') + old_pattern = '' + }) + + // When the user type "Enter", the first alias is clicked + field.keypress(function (event) { + if (event.originalEvent.charCode === 13 && notes.length > 0) { + const li_obj = field.parent().find('ul li').first() + displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field) + li_obj.trigger('click') + } + }) + // When the user type something, the matched aliases are refreshed + field.keyup(function (e) { + field.removeClass('is-invalid') + + if (e.originalEvent.charCode === 13) { return } + + const pattern = field.val() + + // If the pattern is not modified, we don't query the API + if (pattern === old_pattern) { return } + old_pattern = pattern + notes.length = 0 + + // get matched Alias with note associated + if (pattern === '') { + field.tooltip('hide') + notes.length = 0 + return + } + + $.getJSON('/api/note/consumer/?format=json&alias=' + pattern + '&search=user|club', + function (consumers) { + // The response arrived too late, we stop the request + if (pattern !== $('#' + field_id).val()) { return } + + // Build tooltip content + let aliases_matched_html = '<ul class="list-group list-group-flush">' + consumers.results.forEach(function (consumer) { + const note = consumer.note + note.email_confirmed = consumer.email_confirmed + if (consumer.hasOwnProperty('membership') && consumer.membership) { note.membership = consumer.membership } else { note.membership = undefined } + const extra_css = displayStyle(note) + aliases_matched_html += li(alias_prefix + '_' + consumer.id, + consumer.name, + extra_css) + notes.push(note) + }) + aliases_matched_html += '</ul>' + + // Show tooltip + field.attr('data-original-title', aliases_matched_html).tooltip('show') + + consumers.results.forEach(function (consumer) { + const consumer_obj = $('#' + alias_prefix + '_' + consumer.id) + consumer_obj.hover(function () { + displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field) + }) + consumer_obj.click(function () { + var disp = null + notes_display.forEach(function (d) { + // We compare the alias ids + if (d.id === consumer.id) { + d.quantity += 1 + disp = d + } + }) + // In the other case, we add a new emitter + if (disp == null) { + disp = { + name: consumer.name, + id: consumer.id, + note: consumer.note, + quantity: 1 + } + notes_display.push(disp) + } + + // If the function alias_click exists, it is called. If it doesn't return true, then the notes are + // note displayed. Useful for a consumption when a button is already clicked + if (alias_click && !alias_click()) { return } + + const note_list = $('#' + note_list_id) + let html = '' + notes_display.forEach(function (disp) { + html += li(note_prefix + '_' + disp.id, + disp.name + + '<span class="badge badge-dark badge-pill">' + + disp.quantity + '</span>', + displayStyle(disp.note)) + }) + + // Emitters are displayed + note_list.html(html) + + // Update tooltip position + field.tooltip('update') + + notes_display.forEach(function (disp) { + const line_obj = $('#' + note_prefix + '_' + disp.id) + // Hover an emitter display also the profile picture + line_obj.hover(function () { + displayNote(disp.note, disp.name, user_note_field, profile_pic_field) + }) + + // When an emitter is clicked, it is removed + line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, + profile_pic_field)) + }) + }) + }) + })// end getJSON alias + }) +}// end function autocomplete // When a validate button is clicked, we switch the validation status function de_validate (id, validated, resourcetype) { - let validate_obj = $("#validate_" + id); - - if (validate_obj.data("pending")) - // The button is already clicked - return; - - let invalidity_reason = $("#invalidity_reason_" + id).val(); - validate_obj.html("<strong style=\"font-size: 16pt;\">⟳</strong>"); - validate_obj.data("pending", true); - - // Perform a PATCH request to the API in order to update the transaction - // If the user has insufficient rights, an error message will appear - $.ajax({ - "url": "/api/note/transaction/transaction/" + id + "/", - type: "PATCH", - dataType: "json", - headers: { - "X-CSRFTOKEN": CSRF_TOKEN - }, - data: { - "resourcetype": resourcetype, - "valid": !validated, - "invalidity_reason": invalidity_reason, - }, - success: function () { - refreshBalance(); - // error if this method doesn't exist. Please define it. - refreshHistory(); - }, - error: function (err) { - let errObj = JSON.parse(err.responseText); - let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]; - if (!error) - error = err.responseText; - addMsg("Une erreur est survenue lors de la validation/dévalidation " + - "de cette transaction : " + error, "danger"); - - refreshBalance(); - // error if this method doesn't exist. Please define it. - refreshHistory(); - } - }); + const validate_obj = $('#validate_' + id) + + if (validate_obj.data('pending')) + // The button is already clicked + { return } + + const invalidity_reason = $('#invalidity_reason_' + id).val() + validate_obj.html('<strong style="font-size: 16pt;">⟳</strong>') + validate_obj.data('pending', true) + + // Perform a PATCH request to the API in order to update the transaction + // If the user has insufficient rights, an error message will appear + $.ajax({ + url: '/api/note/transaction/transaction/' + id + '/', + type: 'PATCH', + dataType: 'json', + headers: { + 'X-CSRFTOKEN': CSRF_TOKEN + }, + data: { + resourcetype: resourcetype, + valid: !validated, + invalidity_reason: invalidity_reason + }, + success: function () { + refreshBalance() + // error if this method doesn't exist. Please define it. + refreshHistory() + }, + error: function (err) { + const errObj = JSON.parse(err.responseText) + let error = errObj.detail ? errObj.detail : errObj.non_field_errors + if (!error) { error = err.responseText } + addMsg('Une erreur est survenue lors de la validation/dévalidation ' + + 'de cette transaction : ' + error, 'danger') + + refreshBalance() + // error if this method doesn't exist. Please define it. + refreshHistory() + } + }) } /** @@ -404,10 +379,10 @@ function de_validate (id, validated, resourcetype) { * @param wait Debounced milliseconds */ function debounce (callback, wait) { - let timeout; - return (...args) => { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => callback.apply(context, args), wait); - }; + let timeout + return (...args) => { + const context = this + clearTimeout(timeout) + timeout = setTimeout(() => callback.apply(context, args), wait) + } } diff --git a/note_kfet/static/js/consos.js b/note_kfet/static/js/consos.js index 25e8113e4a855b2b22de91f0a19f7cabe5f51ebe..5a7e61449816900531a65c51224515c2f28bc2cb 100644 --- a/note_kfet/static/js/consos.js +++ b/note_kfet/static/js/consos.js @@ -2,95 +2,92 @@ // SPDX-License-Identifier: GPL-3.0-or-later // When a transaction is performed, lock the interface to prevent spam clicks. -var LOCK = false; +var LOCK = false /** * Refresh the history table on the consumptions page. */ -function refreshHistory() { - $("#history").load("/note/consos/ #history"); - $("#most_used").load("/note/consos/ #most_used"); +function refreshHistory () { + $('#history').load('/note/consos/ #history') + $('#most_used').load('/note/consos/ #most_used') } -$(document).ready(function() { - // If hash of a category in the URL, then select this category - // else select the first one - if (location.hash) { - $("a[href='" + location.hash + "']").tab("show"); - } else { - $("a[data-toggle='tab']").first().tab("show"); - } +$(document).ready(function () { + // If hash of a category in the URL, then select this category + // else select the first one + if (location.hash) { + $("a[href='" + location.hash + "']").tab('show') + } else { + $("a[data-toggle='tab']").first().tab('show') + } - // When selecting a category, change URL - $(document.body).on("click", "a[data-toggle='tab']", function() { - location.hash = this.getAttribute("href"); - }); + // When selecting a category, change URL + $(document.body).on('click', "a[data-toggle='tab']", function () { + location.hash = this.getAttribute('href') + }) - // Switching in double consumptions mode should update the layout - $("#double_conso").change(function() { - $("#consos_list_div").removeClass('d-none'); - $("#user_select_div").attr('class', 'col-xl-4'); - $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); + // Switching in double consumptions mode should update the layout + $('#double_conso').change(function () { + $('#consos_list_div').removeClass('d-none') + $('#user_select_div').attr('class', 'col-xl-4') + $('#infos_div').attr('class', 'col-sm-5 col-xl-6') - let note_list_obj = $("#note_list"); - if (buttons.length > 0 && note_list_obj.text().length > 0) { - $("#consos_list").html(note_list_obj.html()); - note_list_obj.html(""); + const note_list_obj = $('#note_list') + if (buttons.length > 0 && note_list_obj.text().length > 0) { + $('#consos_list').html(note_list_obj.html()) + note_list_obj.html('') - buttons.forEach(function(button) { - $("#conso_button_" + button.id).click(function() { - if (LOCK) - return; - removeNote(button, "conso_button", buttons,"consos_list")(); - }); - }); - } - }); + buttons.forEach(function (button) { + $('#conso_button_' + button.id).click(function () { + if (LOCK) { return } + removeNote(button, 'conso_button', buttons, 'consos_list')() + }) + }) + } + }) - $("#single_conso").change(function() { - $("#consos_list_div").addClass('d-none'); - $("#user_select_div").attr('class', 'col-xl-7'); - $("#infos_div").attr('class', 'col-sm-5 col-md-4'); + $('#single_conso').change(function () { + $('#consos_list_div').addClass('d-none') + $('#user_select_div').attr('class', 'col-xl-7') + $('#infos_div').attr('class', 'col-sm-5 col-md-4') - let consos_list_obj = $("#consos_list"); - if (buttons.length > 0) { - if (notes_display.length === 0 && consos_list_obj.text().length > 0) { - $("#note_list").html(consos_list_obj.html()); - consos_list_obj.html(""); - buttons.forEach(function(button) { - $("#conso_button_" + button.id).click(function() { - if (LOCK) - return; - removeNote(button, "conso_button", buttons,"note_list")(); - }); - }); - } - else { - buttons.length = 0; - consos_list_obj.html(""); - } - } - }); + const consos_list_obj = $('#consos_list') + if (buttons.length > 0) { + if (notes_display.length === 0 && consos_list_obj.text().length > 0) { + $('#note_list').html(consos_list_obj.html()) + consos_list_obj.html('') + buttons.forEach(function (button) { + $('#conso_button_' + button.id).click(function () { + if (LOCK) { return } + removeNote(button, 'conso_button', buttons, 'note_list')() + }) + }) + } else { + buttons.length = 0 + consos_list_obj.html('') + } + } + }) - // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS - $("label[for='double_conso']").removeClass('active'); + // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS + $("label[for='double_conso']").removeClass('active') - $("#consume_all").click(consumeAll); -}); + $('#consume_all').click(consumeAll) +}) -notes = []; -notes_display = []; -buttons = []; +notes = [] +notes_display = [] +buttons = [] // When the user searches an alias, we update the auto-completion -autoCompleteNote("note", "note_list", notes, notes_display, - "alias", "note", "user_note", "profile_pic", function() { - if (buttons.length > 0 && $("#single_conso").is(":checked")) { - consumeAll(); - return false; - } - return true; - }); +autoCompleteNote('note', 'note_list', notes, notes_display, + 'alias', 'note', 'user_note', 'profile_pic', function () { + if (buttons.length > 0 && $('#single_conso').is(':checked')) { + consumeAll() + return false + } + return true + }) /** * Add a transaction from a button. @@ -102,103 +99,98 @@ autoCompleteNote("note", "note_list", notes, notes_display, * @param template_id The identifier of the button * @param template_name The name of the button */ -function addConso(dest, amount, type, category_id, category_name, template_id, template_name) { - var button = null; - buttons.forEach(function(b) { - if (b.id === template_id) { - b.quantity += 1; - button = b; - } - }); - if (button == null) { - button = { - id: template_id, - name: template_name, - dest: dest, - quantity: 1, - amount: amount, - type: type, - category_id: category_id, - category_name: category_name - }; - buttons.push(button); +function addConso (dest, amount, type, category_id, category_name, template_id, template_name) { + var button = null + buttons.forEach(function (b) { + if (b.id === template_id) { + b.quantity += 1 + button = b } + }) + if (button == null) { + button = { + id: template_id, + name: template_name, + dest: dest, + quantity: 1, + amount: amount, + type: type, + category_id: category_id, + category_name: category_name + } + buttons.push(button) + } - let dc_obj = $("#double_conso"); - if (dc_obj.is(":checked") || notes_display.length === 0) { - let list = dc_obj.is(":checked") ? "consos_list" : "note_list"; - let html = ""; - buttons.forEach(function(button) { - html += li("conso_button_" + button.id, button.name - + "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>"); - }); + const dc_obj = $('#double_conso') + if (dc_obj.is(':checked') || notes_display.length === 0) { + const list = dc_obj.is(':checked') ? 'consos_list' : 'note_list' + let html = '' + buttons.forEach(function (button) { + html += li('conso_button_' + button.id, button.name + + '<span class="badge badge-dark badge-pill">' + button.quantity + '</span>') + }) - $("#" + list).html(html); + $('#' + list).html(html) - buttons.forEach(function(button) { - $("#conso_button_" + button.id).click(function() { - if (LOCK) - return; - removeNote(button, "conso_button", buttons, list)(); - }); - }); - } - else - consumeAll(); + buttons.forEach(function (button) { + $('#conso_button_' + button.id).click(function () { + if (LOCK) { return } + removeNote(button, 'conso_button', buttons, list)() + }) + }) + } else { consumeAll() } } /** * Reset the page as its initial state. */ -function reset() { - notes_display.length = 0; - notes.length = 0; - buttons.length = 0; - $("#note_list").html(""); - $("#consos_list").html(""); - $("#note").val(""); - $("#note").attr("data-original-title", "").tooltip("hide"); - $("#profile_pic").attr("src", "/static/member/img/default_picture.png"); - $("#profile_pic_link").attr("href", "#"); - refreshHistory(); - refreshBalance(); - LOCK = false; +function reset () { + notes_display.length = 0 + notes.length = 0 + buttons.length = 0 + $('#note_list').html('') + $('#consos_list').html('') + $('#note').val('') + $('#note').attr('data-original-title', '').tooltip('hide') + $('#profile_pic').attr('src', '/static/member/img/default_picture.png') + $('#profile_pic_link').attr('href', '#') + refreshHistory() + refreshBalance() + LOCK = false } - /** * Apply all transactions: all notes in `notes` buy each item in `buttons` */ -function consumeAll() { - if (LOCK) - return; +function consumeAll () { + if (LOCK) { return } - LOCK = true; + LOCK = true - let error = false; + let error = false - if (notes_display.length === 0) { - $("#note").addClass('is-invalid'); - $("#note_list").html(li("", "<strong>Ajoutez des émetteurs.</strong>", "text-danger")); - error = true; - } + if (notes_display.length === 0) { + $('#note').addClass('is-invalid') + $('#note_list').html(li('', '<strong>Ajoutez des émetteurs.</strong>', 'text-danger')) + error = true + } - if (buttons.length === 0) { - $("#consos_list").html(li("", "<strong>Ajoutez des consommations.</strong>", "text-danger")); - error = true; - } + if (buttons.length === 0) { + $('#consos_list').html(li('', '<strong>Ajoutez des consommations.</strong>', 'text-danger')) + error = true + } - if (error) { - LOCK = false; - return; - } + if (error) { + LOCK = false + return + } - notes_display.forEach(function(note_display) { - buttons.forEach(function(button) { - consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, - button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); - }); - }); + notes_display.forEach(function (note_display) { + buttons.forEach(function (button) { + consume(note_display.note, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, + button.name + ' (' + button.category_name + ')', button.type, button.category_id, button.id) + }) + }) } /** @@ -213,58 +205,60 @@ function consumeAll() { * @param category The category id of the button (type: int) * @param template The button id (type: int) */ -function consume(source, source_alias, dest, quantity, amount, reason, type, category, template) { - $.post("/api/note/transaction/transaction/", +function consume (source, source_alias, dest, quantity, amount, reason, type, category, template) { + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: quantity, + amount: amount, + reason: reason, + valid: true, + polymorphic_ctype: type, + resourcetype: 'RecurrentTransaction', + source: source.id, + source_alias: source_alias, + destination: dest, + template: template + }) + .done(function () { + if (!isNaN(source.balance)) { + const newBalance = source.balance - quantity * amount + if (newBalance <= -5000) { + addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + + 'succès, mais la note émettrice ' + source_alias + ' est en négatif sévère.', + 'danger', 30000) + } else if (newBalance < 0) { + addMsg('Attention, La transaction depuis la note ' + source_alias + ' a été réalisée avec ' + + 'succès, mais la note émettrice ' + source_alias + ' est en négatif.', + 'warning', 30000) + } + if (source.membership && source.membership.date_end < new Date().toISOString()) { + addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", + 'danger', 30000) + } + } + reset() + }).fail(function (e) { + $.post('/api/note/transaction/transaction/', { - "csrfmiddlewaretoken": CSRF_TOKEN, - "quantity": quantity, - "amount": amount, - "reason": reason, - "valid": true, - "polymorphic_ctype": type, - "resourcetype": "RecurrentTransaction", - "source": source.id, - "source_alias": source_alias, - "destination": dest, - "template": template - }) - .done(function () { - if (!isNaN(source.balance)) { - let newBalance = source.balance - quantity * amount; - if (newBalance <= -5000) - addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " + - "succès, mais la note émettrice " + source_alias + " est en négatif sévère.", - "danger", 30000); - else if (newBalance < 0) - addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " + - "succès, mais la note émettrice " + source_alias + " est en négatif.", - "warning", 30000); - if (source.membership && source.membership.date_end < new Date().toISOString()) - addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.", - "danger", 30000); - } - reset(); - }).fail(function (e) { - $.post("/api/note/transaction/transaction/", - { - "csrfmiddlewaretoken": CSRF_TOKEN, - "quantity": quantity, - "amount": amount, - "reason": reason, - "valid": false, - "invalidity_reason": "Solde insuffisant", - "polymorphic_ctype": type, - "resourcetype": "RecurrentTransaction", - "source": source, - "source_alias": source_alias, - "destination": dest, - "template": template - }).done(function() { - reset(); - addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000); - }).fail(function () { - reset(); - errMsg(e.responseJSON); - }); - }); + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: quantity, + amount: amount, + reason: reason, + valid: false, + invalidity_reason: 'Solde insuffisant', + polymorphic_ctype: type, + resourcetype: 'RecurrentTransaction', + source: source, + source_alias: source_alias, + destination: dest, + template: template + }).done(function () { + reset() + addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", 'danger', 10000) + }).fail(function () { + reset() + errMsg(e.responseJSON) + }) + }) } diff --git a/note_kfet/static/js/dynamic-formset.js b/note_kfet/static/js/dynamic-formset.js index c6ff3328d5aa0923d732f8599fe929c6ccb2895c..88d01ecac5e7b1b67dd9382dd5ff46bedc95e2f6 100644 --- a/note_kfet/static/js/dynamic-formset.js +++ b/note_kfet/static/js/dynamic-formset.js @@ -9,241 +9,240 @@ * Licensed under the New BSD License * See: http://www.opensource.org/licenses/bsd-license.php */ -;(function($) { - $.fn.formset = function(opts) - { - var options = $.extend({}, $.fn.formset.defaults, opts), - flatExtraClasses = options.extraClasses.join(' '), - totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'), - maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'), - minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'), - childElementSelector = 'input,select,textarea,label,div', - $$ = $(this), - - applyExtraClasses = function(row, ndx) { - if (options.extraClasses) { - row.removeClass(flatExtraClasses); - row.addClass(options.extraClasses[ndx % options.extraClasses.length]); - } - }, - - updateElementIndex = function(elem, prefix, ndx) { - var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'), - replacement = prefix + '-' + ndx + '-'; - if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement)); - if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement)); - if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)); - }, - - hasChildElements = function(row) { - return row.find(childElementSelector).length > 0; - }, - - showAddButton = function() { - return maxForms.length == 0 || // For Django versions pre 1.2 - (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0)); - }, - - /** +;(function ($) { + $.fn.formset = function (opts) { + var options = $.extend({}, $.fn.formset.defaults, opts) + var flatExtraClasses = options.extraClasses.join(' ') + var totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS') + var maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS') + var minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS') + var childElementSelector = 'input,select,textarea,label,div' + var $$ = $(this) + + var applyExtraClasses = function (row, ndx) { + if (options.extraClasses) { + row.removeClass(flatExtraClasses) + row.addClass(options.extraClasses[ndx % options.extraClasses.length]) + } + } + + var updateElementIndex = function (elem, prefix, ndx) { + var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-') + var replacement = prefix + '-' + ndx + '-' + if (elem.attr('for')) elem.attr('for', elem.attr('for').replace(idRegex, replacement)) + if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement)) + if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)) + } + + var hasChildElements = function (row) { + return row.find(childElementSelector).length > 0 + } + + var showAddButton = function () { + return maxForms.length === 0 || // For Django versions pre 1.2 + (maxForms.val() === '' || (maxForms.val() - totalForms.val() > 0)) + } + + /** * Indicates whether delete link(s) can be displayed - when total forms > min forms */ - showDeleteLinks = function() { - return minForms.length == 0 || // For Django versions pre 1.7 - (minForms.val() == '' || (totalForms.val() - minForms.val() > 0)); - }, - - insertDeleteLink = function(row) { - var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), - addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); - - var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>'; - if (options.deleteContainerClass) { - // If we have a specific container for the remove button, - // place it as the last child of that container: - row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML); - } else if (row.is('TR')) { - // If the forms are laid out in table rows, insert - // the remove button into the last table cell: - row.children('td:last').append(delButtonHTML); - } else if (row.is('UL') || row.is('OL')) { - // If they're laid out as an ordered/unordered list, - // insert an <li> after the last list item: - row.append('<li>' + delButtonHTML + '</li>'); - } else { - // Otherwise, just insert the remove button as the - // last child element of the form's container: - row.append(delButtonHTML); - } - - // Check if we're under the minimum number of forms - not to display delete link at rendering - if (!showDeleteLinks()){ - row.find('a.' + delCssSelector).hide(); - } - - row.find('a.' + delCssSelector).click(function() { - var row = $(this).parents('.' + options.formCssClass), - del = row.find('input:hidden[id $= "-DELETE"]'), - buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'), - forms; - if (del.length) { - // We're dealing with an inline formset. - // Rather than remove this form from the DOM, we'll mark it as deleted - // and hide it, then let Django handle the deleting: - del.val('on'); - row.hide(); - forms = $('.' + options.formCssClass).not(':hidden'); - } else { - row.remove(); - // Update the TOTAL_FORMS count: - forms = $('.' + options.formCssClass).not('.formset-custom-template'); - totalForms.val(forms.length); - } - for (var i=0, formCount=forms.length; i<formCount; i++) { - // Apply `extraClasses` to form rows so they're nicely alternating: - applyExtraClasses(forms.eq(i), i); - if (!del.length) { - // Also update names and IDs for all child controls (if this isn't - // a delete-able inline formset) so they remain in sequence: - forms.eq(i).find(childElementSelector).each(function() { - updateElementIndex($(this), options.prefix, i); - }); - } - } - // Check if we've reached the minimum number of forms - hide all delete link(s) - if (!showDeleteLinks()){ - $('a.' + delCssSelector).each(function(){$(this).hide();}); - } - // Check if we need to show the add button: - if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show(); - // If a post-delete callback was provided, call it with the deleted form: - if (options.removed) options.removed(row); - return false; - }); - }; - - $$.each(function(i) { - var row = $(this), - del = row.find('input:checkbox[id $= "-DELETE"]'); - if (del.length) { - // If you specify "can_delete = True" when creating an inline formset, - // Django adds a checkbox to each form in the formset. - // Replace the default checkbox with a hidden field: - if (del.is(':checked')) { - // If an inline formset containing deleted forms fails validation, make sure - // we keep the forms hidden (thanks for the bug report and suggested fix Mike) - del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />'); - row.hide(); - } else { - del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />'); - } - // Hide any labels associated with the DELETE checkbox: - $('label[for="' + del.attr('id') + '"]').hide(); - del.remove(); - } - if (hasChildElements(row)) { - row.addClass(options.formCssClass); - if (row.is(':visible')) { - insertDeleteLink(row); - applyExtraClasses(row, i); - } - } - }); - - if ($$.length) { - var hideAddButton = !showAddButton(), - addButton, template; - if (options.formTemplate) { - // If a form template was specified, we'll clone it to generate new form instances: - template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate); - template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template'); - template.find(childElementSelector).each(function() { - updateElementIndex($(this), options.prefix, '__prefix__'); - }); - insertDeleteLink(template); - } else { - // Otherwise, use the last form in the formset; this works much better if you've got - // extra (>= 1) forms (thnaks to justhamade for pointing this out): - if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide(); - template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id'); - template.find('input:hidden[id $= "-DELETE"]').remove(); - // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): - template.find(childElementSelector).not(options.keepFieldValues).each(function() { - var elem = $(this); - // If this is a checkbox or radiobutton, uncheck it. - // This fixes Issue 1, reported by Wilson.Andrew.J: - if (elem.is('input:checkbox') || elem.is('input:radio')) { - elem.attr('checked', false); - } else { - elem.val(''); - } - }); - } - // FIXME: Perhaps using $.data would be a better idea? - options.formTemplate = template; - - var addButtonHTML = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>'; - if (options.addContainerClass) { - // If we have a specific container for the "add" button, - // place it as the last child of that container: - var addContainer = $('[class*="' + options.addContainerClass + '"'); - addContainer.append(addButtonHTML); - addButton = addContainer.find('[class="' + options.addCssClass + '"]'); - } else if ($$.is('TR')) { - // If forms are laid out as table rows, insert the - // "add" button in a new table row: - var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| - buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add'); - $$.parent().append(buttonRow); - addButton = buttonRow.find('a'); - } else { - // Otherwise, insert it immediately after the last form: - $$.filter(':last').after(addButtonHTML); - addButton = $$.filter(':last').next(); - } - - if (hideAddButton) addButton.hide(); - - addButton.click(function() { - var formCount = parseInt(totalForms.val()), - row = options.formTemplate.clone(true).removeClass('formset-custom-template'), - buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this), - delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'); - applyExtraClasses(row, formCount); - row.insertBefore(buttonRow).show(); - row.find(childElementSelector).each(function() { - updateElementIndex($(this), options.prefix, formCount); - }); - totalForms.val(formCount + 1); - // Check if we're above the minimum allowed number of forms -> show all delete link(s) - if (showDeleteLinks()){ - $('a.' + delCssSelector).each(function(){$(this).show();}); - } - // Check if we've exceeded the maximum allowed number of forms: - if (!showAddButton()) buttonRow.hide(); - // If a post-add callback was supplied, call it with the added form: - if (options.added) options.added(row); - return false; - }); + var showDeleteLinks = function () { + return minForms.length === 0 || // For Django versions pre 1.7 + (minForms.val() === '' || (totalForms.val() - minForms.val() > 0)) + } + + var insertDeleteLink = function (row) { + var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.') + var addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.') + + var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText + '</a>' + if (options.deleteContainerClass) { + // If we have a specific container for the remove button, + // place it as the last child of that container: + row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML) + } else if (row.is('TR')) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children('td:last').append(delButtonHTML) + } else if (row.is('UL') || row.is('OL')) { + // If they're laid out as an ordered/unordered list, + // insert an <li> after the last list item: + row.append('<li>' + delButtonHTML + '</li>') + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.append(delButtonHTML) + } + + // Check if we're under the minimum number of forms - not to display delete link at rendering + if (!showDeleteLinks()) { + row.find('a.' + delCssSelector).hide() + } + + row.find('a.' + delCssSelector).click(function () { + var row = $(this).parents('.' + options.formCssClass) + var del = row.find('input:hidden[id $= "-DELETE"]') + var buttonRow = row.siblings('a.' + addCssSelector + ', .' + options.formCssClass + '-add') + var forms + if (del.length) { + // We're dealing with an inline formset. + // Rather than remove this form from the DOM, we'll mark it as deleted + // and hide it, then let Django handle the deleting: + del.val('on') + row.hide() + forms = $('.' + options.formCssClass).not(':hidden') + } else { + row.remove() + // Update the TOTAL_FORMS count: + forms = $('.' + options.formCssClass).not('.formset-custom-template') + totalForms.val(forms.length) } - - return $$; - }; - - /* Setup plugin defaults */ - $.fn.formset.defaults = { - prefix: 'form', // The form prefix for your django formset - formTemplate: null, // The jQuery selection cloned to generate new form instances - addText: 'add another', // Text for the add link - deleteText: 'remove', // Text for the delete link - addContainerClass: null, // Container CSS class for the add link - deleteContainerClass: null, // Container CSS class for the delete link - addCssClass: 'add-row', // CSS class applied to the add link - deleteCssClass: 'delete-row', // CSS class applied to the delete link - formCssClass: 'dynamic-form', // CSS class applied to each form in a formset - extraClasses: [], // Additional CSS classes, which will be applied to each form in turn - keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned - added: null, // Function called each time a new form is added - removed: null, // Function called each time a form is deleted - hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button) - }; -})(jQuery); + for (var i = 0, formCount = forms.length; i < formCount; i++) { + // Apply `extraClasses` to form rows so they're nicely alternating: + applyExtraClasses(forms.eq(i), i) + if (!del.length) { + // Also update names and IDs for all child controls (if this isn't + // a delete-able inline formset) so they remain in sequence: + forms.eq(i).find(childElementSelector).each(function () { + updateElementIndex($(this), options.prefix, i) + }) + } + } + // Check if we've reached the minimum number of forms - hide all delete link(s) + if (!showDeleteLinks()) { + $('a.' + delCssSelector).each(function () { $(this).hide() }) + } + // Check if we need to show the add button: + if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show() + // If a post-delete callback was provided, call it with the deleted form: + if (options.removed) options.removed(row) + return false + }) + } + + $$.each(function (i) { + var row = $(this) + var del = row.find('input:checkbox[id $= "-DELETE"]') + if (del.length) { + // If you specify "can_delete = True" when creating an inline formset, + // Django adds a checkbox to each form in the formset. + // Replace the default checkbox with a hidden field: + if (del.is(':checked')) { + // If an inline formset containing deleted forms fails validation, make sure + // we keep the forms hidden (thanks for the bug report and suggested fix Mike) + del.before('<input type="hidden" name="' + del.attr('name') + '" id="' + del.attr('id') + '" value="on" />') + row.hide() + } else { + del.before('<input type="hidden" name="' + del.attr('name') + '" id="' + del.attr('id') + '" />') + } + // Hide any labels associated with the DELETE checkbox: + $('label[for="' + del.attr('id') + '"]').hide() + del.remove() + } + if (hasChildElements(row)) { + row.addClass(options.formCssClass) + if (row.is(':visible')) { + insertDeleteLink(row) + applyExtraClasses(row, i) + } + } + }) + + if ($$.length) { + var hideAddButton = !showAddButton() + var addButton; var template + if (options.formTemplate) { + // If a form template was specified, we'll clone it to generate new form instances: + template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate) + template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template') + template.find(childElementSelector).each(function () { + updateElementIndex($(this), options.prefix, '__prefix__') + }) + insertDeleteLink(template) + } else { + // Otherwise, use the last form in the formset; this works much better if you've got + // extra (>= 1) forms (thnaks to justhamade for pointing this out): + if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide() + template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id') + template.find('input:hidden[id $= "-DELETE"]').remove() + // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): + template.find(childElementSelector).not(options.keepFieldValues).each(function () { + var elem = $(this) + // If this is a checkbox or radiobutton, uncheck it. + // This fixes Issue 1, reported by Wilson.Andrew.J: + if (elem.is('input:checkbox') || elem.is('input:radio')) { + elem.attr('checked', false) + } else { + elem.val('') + } + }) + } + // FIXME: Perhaps using $.data would be a better idea? + options.formTemplate = template + + var addButtonHTML = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>' + if (options.addContainerClass) { + // If we have a specific container for the "add" button, + // place it as the last child of that container: + var addContainer = $('[class*="' + options.addContainerClass + '"') + addContainer.append(addButtonHTML) + addButton = addContainer.find('[class="' + options.addCssClass + '"]') + } else if ($$.is('TR')) { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + var numCols = $$.eq(0).children().length // This is a bit of an assumption :| + var buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add') + $$.parent().append(buttonRow) + addButton = buttonRow.find('a') + } else { + // Otherwise, insert it immediately after the last form: + $$.filter(':last').after(addButtonHTML) + addButton = $$.filter(':last').next() + } + + if (hideAddButton) addButton.hide() + + addButton.click(function () { + var formCount = parseInt(totalForms.val()) + var row = options.formTemplate.clone(true).removeClass('formset-custom-template') + var buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this) + var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.') + applyExtraClasses(row, formCount) + row.insertBefore(buttonRow).show() + row.find(childElementSelector).each(function () { + updateElementIndex($(this), options.prefix, formCount) + }) + totalForms.val(formCount + 1) + // Check if we're above the minimum allowed number of forms -> show all delete link(s) + if (showDeleteLinks()) { + $('a.' + delCssSelector).each(function () { $(this).show() }) + } + // Check if we've exceeded the maximum allowed number of forms: + if (!showAddButton()) buttonRow.hide() + // If a post-add callback was supplied, call it with the added form: + if (options.added) options.added(row) + return false + }) + } + + return $$ + } + + /* Setup plugin defaults */ + $.fn.formset.defaults = { + prefix: 'form', // The form prefix for your django formset + formTemplate: null, // The jQuery selection cloned to generate new form instances + addText: 'add another', // Text for the add link + deleteText: 'remove', // Text for the delete link + addContainerClass: null, // Container CSS class for the add link + deleteContainerClass: null, // Container CSS class for the delete link + addCssClass: 'add-row', // CSS class applied to the add link + deleteCssClass: 'delete-row', // CSS class applied to the delete link + formCssClass: 'dynamic-form', // CSS class applied to each form in a formset + extraClasses: [], // Additional CSS classes, which will be applied to each form in turn + keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned + added: null, // Function called each time a new form is added + removed: null, // Function called each time a form is deleted + hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button) + } +})(jQuery) diff --git a/note_kfet/static/js/konami.js b/note_kfet/static/js/konami.js index a430a4b64594130fd87a9d53e1bda543b9af0ee0..add7fb0bbac0f17d92744c2bf87a71e5dc6d8669 100644 --- a/note_kfet/static/js/konami.js +++ b/note_kfet/static/js/konami.js @@ -6,40 +6,40 @@ let cursor = 0 const KONAMI_CODE = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65] -function afterKonami() { - // Load Rythm.js - var rythmScript = document.createElement('script') - rythmScript.setAttribute('src','//unpkg.com/rythm.js@2.2.5/rythm.min.js') - document.head.appendChild(rythmScript) +function afterKonami () { + // Load Rythm.js + var rythmScript = document.createElement('script') + rythmScript.setAttribute('src', '//unpkg.com/rythm.js@2.2.5/rythm.min.js') + document.head.appendChild(rythmScript) - rythmScript.addEventListener('load', function() { - // Ker-Lyon audio courtesy of @adalan, ker-lyon.fr - const audioElement = new Audio('/static/song/konami.ogg') - audioElement.loop = true - audioElement.play() + rythmScript.addEventListener('load', function () { + // Ker-Lyon audio courtesy of @adalan, ker-lyon.fr + const audioElement = new Audio('/static/song/konami.ogg') + audioElement.loop = true + audioElement.play() - const rythm = new Rythm() - rythm.connectExternalAudioElement(audioElement) - rythm.addRythm('card', 'pulse', 50, 50, { - min: 1, - max: 1.1 - }) - rythm.addRythm('d-flex', 'color', 50, 50, { - from: [64,64,64], - to:[128,64,128] - }) - rythm.addRythm('nav-link', 'jump', 150, 50, { - min: 0, - max: 10 - }) - rythm.start() - }); + const rythm = new Rythm() + rythm.connectExternalAudioElement(audioElement) + rythm.addRythm('card', 'pulse', 50, 50, { + min: 1, + max: 1.1 + }) + rythm.addRythm('d-flex', 'color', 50, 50, { + from: [64, 64, 64], + to: [128, 64, 128] + }) + rythm.addRythm('nav-link', 'jump', 150, 50, { + min: 0, + max: 10 + }) + rythm.start() + }) } // Register custom event document.addEventListener('keydown', (e) => { - cursor = (e.keyCode == KONAMI_CODE[cursor]) ? cursor + 1 : 0; - if (cursor == KONAMI_CODE.length) { - afterKonami() - } -}); + cursor = (e.keyCode === KONAMI_CODE[cursor]) ? cursor + 1 : 0 + if (cursor === KONAMI_CODE.length) { + afterKonami() + } +}) diff --git a/note_kfet/static/js/transfer.js b/note_kfet/static/js/transfer.js index e22d2b3f97fdc1bd825c77037fd643372c0e30e2..db9cf9acfae4dcaad82b1367578ba6f612c82722 100644 --- a/note_kfet/static/js/transfer.js +++ b/note_kfet/static/js/transfer.js @@ -1,440 +1,422 @@ -var LOCK = false; +var LOCK = false -sources = []; -sources_notes_display = []; -dests = []; -dests_notes_display = []; +sources = [] +sources_notes_display = [] +dests = [] +dests_notes_display = [] -function refreshHistory() { - $("#history").load("/note/transfer/ #history"); +function refreshHistory () { + $('#history').load('/note/transfer/ #history') } -function reset(refresh=true) { - sources_notes_display.length = 0; - sources.length = 0; - dests_notes_display.length = 0; - dests.length = 0; - $("#source_note_list").html(""); - $("#dest_note_list").html(""); - let source_field = $("#source_note"); - source_field.val(""); - let event = jQuery.Event("keyup"); - event.originalEvent = {charCode: 97}; - source_field.trigger(event); - source_field.removeClass('is-invalid'); - source_field.attr("data-original-title", "").tooltip("hide"); - let dest_field = $("#dest_note"); - dest_field.val(""); - dest_field.trigger(event); - dest_field.removeClass('is-invalid'); - dest_field.attr("data-original-title", "").tooltip("hide"); - let amount_field = $("#amount"); - amount_field.val(""); - amount_field.removeClass('is-invalid'); - $("#amount-required").html(""); - let reason_field = $("#reason"); - reason_field.val(""); - reason_field.removeClass('is-invalid'); - $("#reason-required").html(""); - $("#last_name").val(""); - $("#first_name").val(""); - $("#bank").val(""); - $("#user_note").val(""); - $("#profile_pic").attr("src", "/static/member/img/default_picture.png"); - $("#profile_pic_link").attr("href", "#"); - if (refresh) { - refreshBalance(); - refreshHistory(); - } - - LOCK = false; +function reset (refresh = true) { + sources_notes_display.length = 0 + sources.length = 0 + dests_notes_display.length = 0 + dests.length = 0 + $('#source_note_list').html('') + $('#dest_note_list').html('') + const source_field = $('#source_note') + source_field.val('') + const event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_field.trigger(event) + source_field.removeClass('is-invalid') + source_field.attr('data-original-title', '').tooltip('hide') + const dest_field = $('#dest_note') + dest_field.val('') + dest_field.trigger(event) + dest_field.removeClass('is-invalid') + dest_field.attr('data-original-title', '').tooltip('hide') + const amount_field = $('#amount') + amount_field.val('') + amount_field.removeClass('is-invalid') + $('#amount-required').html('') + const reason_field = $('#reason') + reason_field.val('') + reason_field.removeClass('is-invalid') + $('#reason-required').html('') + $('#last_name').val('') + $('#first_name').val('') + $('#bank').val('') + $('#user_note').val('') + $('#profile_pic').attr('src', '/static/member/img/default_picture.png') + $('#profile_pic_link').attr('href', '#') + if (refresh) { + refreshBalance() + refreshHistory() + } + + LOCK = false } -$(document).ready(function() { - /** +$(document).ready(function () { + /** * If we are in credit/debit mode, check that only one note is entered. * More over, get first name and last name to autocomplete fields. */ - function checkUniqueNote() { - if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { - let arr = $("#type_credit").is(":checked") ? dests_notes_display : sources_notes_display; - - if (arr.length === 0) - return; - - let last = arr[arr.length - 1]; - arr.length = 0; - arr.push(last); - - last.quantity = 1; - - if (!last.note.user) { - $.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) { - last.note.user = note.user; - $.getJSON("/api/user/" + last.note.user + "/", function(user) { - $("#last_name").val(user.last_name); - $("#first_name").val(user.first_name); - }); - }); - } - else { - $.getJSON("/api/user/" + last.note.user + "/", function(user) { - $("#last_name").val(user.last_name); - $("#first_name").val(user.first_name); - }); - } - } - - return true; - } - - autoCompleteNote("source_note", "source_note_list", sources, sources_notes_display, - "source_alias", "source_note", "user_note", "profile_pic", checkUniqueNote); - autoCompleteNote("dest_note", "dest_note_list", dests, dests_notes_display, - "dest_alias", "dest_note", "user_note", "profile_pic", checkUniqueNote); - - let source = $("#source_note"); - let dest = $("#dest_note"); - - $("#type_transfer").change(function() { - if (LOCK) - return; - - $("#source_me_div").removeClass('d-none'); - $("#source_note").removeClass('is-invalid'); - $("#dest_note").removeClass('is-invalid'); - $("#special_transaction_div").addClass('d-none'); - source.removeClass('d-none'); - $("#source_note_list").removeClass('d-none'); - $("#credit_type").addClass('d-none'); - dest.removeClass('d-none'); - $("#dest_note_list").removeClass('d-none'); - $("#debit_type").addClass('d-none'); - - $("#source_note_label").text(select_emitters_label); - $("#dest_note_label").text(select_receveirs_label); - - location.hash = "transfer"; - }); - - $("#type_credit").change(function() { - if (LOCK) - return; - - $("#source_me_div").addClass('d-none'); - $("#source_note").removeClass('is-invalid'); - $("#dest_note").removeClass('is-invalid'); - $("#special_transaction_div").removeClass('d-none'); - $("#source_note_list").addClass('d-none'); - $("#dest_note_list").removeClass('d-none'); - source.addClass('d-none'); - source.tooltip('hide'); - $("#credit_type").removeClass('d-none'); - dest.removeClass('d-none'); - dest.val(''); - dest.tooltip('hide'); - $("#debit_type").addClass('d-none'); - - $("#source_note_label").text(transfer_type_label); - $("#dest_note_label").text(select_receveir_label); - - if (dests_notes_display.length > 1) { - $("#dest_note_list").html(''); - dests_notes_display.length = 0; - } - - location.hash = "credit"; - }); - - $("#type_debit").change(function() { - if (LOCK) - return; - - $("#source_me_div").addClass('d-none'); - $("#source_note").removeClass('is-invalid'); - $("#dest_note").removeClass('is-invalid'); - $("#special_transaction_div").removeClass('d-none'); - $("#source_note_list").removeClass('d-none'); - $("#dest_note_list").addClass('d-none'); - source.removeClass('d-none'); - source.val(''); - source.tooltip('hide'); - $("#credit_type").addClass('d-none'); - dest.addClass('d-none'); - dest.tooltip('hide'); - $("#debit_type").removeClass('d-none'); - - $("#source_note_label").text(select_emitter_label); - $("#dest_note_label").text(transfer_type_label); - - if (sources_notes_display.length > 1) { - $("#source_note_list").html(''); - sources_notes_display.length = 0; - } - - location.hash = "debit"; - }); - - $("#credit_type").change(function() { - let type = $("#credit_type option:selected").text(); - if ($("#type_credit").is(":checked")) - source.val(type); - else - dest.val(type); - }); - - // Ensure we begin in transfer mode. Removing these lines may cause problems when reloading. - let type_transfer = $("#type_transfer"); // Default mode - type_transfer.removeAttr('checked'); - $("#type_credit").removeAttr('checked'); - $("#type_debit").removeAttr('checked'); - - if (location.hash) - $("#type_" + location.hash.substr(1)).click(); - else - type_transfer.click(); - - $("#source_me").click(function() { - if (LOCK) - return; - - // Shortcut to set the current user as the only emitter - sources_notes_display.length = 0; - sources.length = 0; - $("#source_note_list").html(""); - - let source_note = $("#source_note"); - source_note.focus(); - source_note.val(""); - let event = jQuery.Event("keyup"); - event.originalEvent = {charCode: 97}; - source_note.trigger(event); - source_note.val(username); - event = jQuery.Event("keyup"); - event.originalEvent = {charCode: 97}; - source_note.trigger(event); - let fill_note = function() { - if (sources.length === 0) { - setTimeout(fill_note, 100); - return; - } - event = jQuery.Event("keypress"); - event.originalEvent = {charCode: 13}; - source_note.trigger(event); - - source_note.tooltip('hide'); - source_note.val(''); - $("#dest_note").focus(); - }; - fill_note(); - }); -}); - -$("#btn_transfer").click(function() { - if (LOCK) - return; - - LOCK = true; - - let error = false; - - let amount_field = $("#amount"); - amount_field.removeClass('is-invalid'); - $("#amount-required").html(""); - - let reason_field = $("#reason"); - reason_field.removeClass('is-invalid'); - $("#reason-required").html(""); - - if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { - amount_field.addClass('is-invalid'); - $("#amount-required").html("<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>"); - error = true; - } - - let amount = Math.floor(100 * amount_field.val()); - if (amount > 2147483647) { - amount_field.addClass('is-invalid'); - $("#amount-required").html("<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>"); - error = true; - } - - if (!reason_field.val()) { - reason_field.addClass('is-invalid'); - $("#reason-required").html("<strong>Ce champ est requis.</strong>"); - error = true; + function checkUniqueNote () { + if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) { + const arr = $('#type_credit').is(':checked') ? dests_notes_display : sources_notes_display + + if (arr.length === 0) { return } + + const last = arr[arr.length - 1] + arr.length = 0 + arr.push(last) + + last.quantity = 1 + + if (!last.note.user) { + $.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) { + last.note.user = note.user + $.getJSON('/api/user/' + last.note.user + '/', function (user) { + $('#last_name').val(user.last_name) + $('#first_name').val(user.first_name) + }) + }) + } else { + $.getJSON('/api/user/' + last.note.user + '/', function (user) { + $('#last_name').val(user.last_name) + $('#first_name').val(user.first_name) + }) + } } - if (!sources_notes_display.length && !$("#type_credit").is(':checked')) { - $("#source_note").addClass('is-invalid'); - error = true; + return true + } + + autoCompleteNote('source_note', 'source_note_list', sources, sources_notes_display, + 'source_alias', 'source_note', 'user_note', 'profile_pic', checkUniqueNote) + autoCompleteNote('dest_note', 'dest_note_list', dests, dests_notes_display, + 'dest_alias', 'dest_note', 'user_note', 'profile_pic', checkUniqueNote) + + const source = $('#source_note') + const dest = $('#dest_note') + + $('#type_transfer').change(function () { + if (LOCK) { return } + + $('#source_me_div').removeClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').addClass('d-none') + source.removeClass('d-none') + $('#source_note_list').removeClass('d-none') + $('#credit_type').addClass('d-none') + dest.removeClass('d-none') + $('#dest_note_list').removeClass('d-none') + $('#debit_type').addClass('d-none') + + $('#source_note_label').text(select_emitters_label) + $('#dest_note_label').text(select_receveirs_label) + + location.hash = 'transfer' + }) + + $('#type_credit').change(function () { + if (LOCK) { return } + + $('#source_me_div').addClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').removeClass('d-none') + $('#source_note_list').addClass('d-none') + $('#dest_note_list').removeClass('d-none') + source.addClass('d-none') + source.tooltip('hide') + $('#credit_type').removeClass('d-none') + dest.removeClass('d-none') + dest.val('') + dest.tooltip('hide') + $('#debit_type').addClass('d-none') + + $('#source_note_label').text(transfer_type_label) + $('#dest_note_label').text(select_receveir_label) + + if (dests_notes_display.length > 1) { + $('#dest_note_list').html('') + dests_notes_display.length = 0 } - if (!dests_notes_display.length && !$("#type_debit").is(':checked')) { - $("#dest_note").addClass('is-invalid'); - error = true; + location.hash = 'credit' + }) + + $('#type_debit').change(function () { + if (LOCK) { return } + + $('#source_me_div').addClass('d-none') + $('#source_note').removeClass('is-invalid') + $('#dest_note').removeClass('is-invalid') + $('#special_transaction_div').removeClass('d-none') + $('#source_note_list').removeClass('d-none') + $('#dest_note_list').addClass('d-none') + source.removeClass('d-none') + source.val('') + source.tooltip('hide') + $('#credit_type').addClass('d-none') + dest.addClass('d-none') + dest.tooltip('hide') + $('#debit_type').removeClass('d-none') + + $('#source_note_label').text(select_emitter_label) + $('#dest_note_label').text(transfer_type_label) + + if (sources_notes_display.length > 1) { + $('#source_note_list').html('') + sources_notes_display.length = 0 } - if (error) { - LOCK = false; - return; + location.hash = 'debit' + }) + + $('#credit_type').change(function () { + const type = $('#credit_type option:selected').text() + if ($('#type_credit').is(':checked')) { source.val(type) } else { dest.val(type) } + }) + + // Ensure we begin in transfer mode. Removing these lines may cause problems when reloading. + const type_transfer = $('#type_transfer') // Default mode + type_transfer.removeAttr('checked') + $('#type_credit').removeAttr('checked') + $('#type_debit').removeAttr('checked') + + if (location.hash) { $('#type_' + location.hash.substr(1)).click() } else { type_transfer.click() } + + $('#source_me').click(function () { + if (LOCK) { return } + + // Shortcut to set the current user as the only emitter + sources_notes_display.length = 0 + sources.length = 0 + $('#source_note_list').html('') + + const source_note = $('#source_note') + source_note.focus() + source_note.val('') + let event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_note.trigger(event) + source_note.val(username) + event = jQuery.Event('keyup') + event.originalEvent = { charCode: 97 } + source_note.trigger(event) + const fill_note = function () { + if (sources.length === 0) { + setTimeout(fill_note, 100) + return + } + event = jQuery.Event('keypress') + event.originalEvent = { charCode: 13 } + source_note.trigger(event) + + source_note.tooltip('hide') + source_note.val('') + $('#dest_note').focus() } - - let reason = reason_field.val(); - - if ($("#type_transfer").is(':checked')) { - // We copy the arrays to ensure that transactions are well-processed even if the form is reset - [...sources_notes_display].forEach(function (source) { - [...dests_notes_display].forEach(function (dest) { - if (source.note.id === dest.note.id) { - addMsg("Attention : la transaction de " + pretty_money(amount) + " de la note " + source.name - + " vers la note " + dest.name + " n'a pas été faite car il s'agit de la même note au départ" + - " et à l'arrivée.","warning", 10000); - LOCK = false; - return; - } - - $.post("/api/note/transaction/transaction/", - { - "csrfmiddlewaretoken": CSRF_TOKEN, - "quantity": source.quantity * dest.quantity, - "amount": amount, - "reason": reason, - "valid": true, - "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, - "resourcetype": "Transaction", - "source": source.note.id, - "source_alias": source.name, - "destination": dest.note.id, - "destination_alias": dest.name - }).done(function () { - if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) - addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.", - "danger", 30000); - if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) - addMsg("Attention : la note destination " + dest.name + " n'est plus adhérente.", - "danger", 30000); - - if (!isNaN(source.note.balance)) { - let newBalance = source.note.balance - source.quantity * dest.quantity * amount; - if (newBalance <= -5000) { - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " - + source.name + " vers la note " + dest.name + " a été fait avec succès, " + - "mais la note émettrice est en négatif sévère.", "danger", 10000); - reset(); - return; - } - else if (newBalance < 0) { - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " - + source.name + " vers la note " + dest.name + " a été fait avec succès, " + - "mais la note émettrice est en négatif.", "warning", 10000); - reset(); - return; - } - } - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name - + " vers la note " + dest.name + " a été fait avec succès !", "success", 10000); - - reset(); - }).fail(function (err) { // do it again but valid = false - let errObj = JSON.parse(err.responseText); - if (errObj["non_field_errors"]) { - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : " + errObj["non_field_errors"], "danger"); - LOCK = false; - return; - } - - $.post("/api/note/transaction/transaction/", - { - "csrfmiddlewaretoken": CSRF_TOKEN, - "quantity": source.quantity * dest.quantity, - "amount": amount, - "reason": reason, - "valid": false, - "invalidity_reason": "Solde insuffisant", - "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, - "resourcetype": "Transaction", - "source": source.note.id, - "source_alias": source.name, - "destination": dest.note.id, - "destination_alias": dest.name - }).done(function () { - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000); - reset(); - }).fail(function (err) { - let errObj = JSON.parse(err.responseText); - let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"] - if (!error) - error = err.responseText; - addMsg("Le transfert de " - + pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name - + " vers la note " + dest.name + " a échoué : " + error, "danger"); - LOCK = false; - }); - }); - }); - }); - } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { - let special_note; - let user_note; - let alias; - let given_reason = reason; - let source_id, dest_id; - if ($("#type_credit").is(':checked')) { - special_note = $("#credit_type").val(); - user_note = dests_notes_display[0].note; - alias = dests_notes_display[0].name; - source_id = special_note; - dest_id = user_note.id; - reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); - if (given_reason.length > 0) - reason += " (" + given_reason + ")"; - } - else { - special_note = $("#debit_type").val(); - user_note = sources_notes_display[0].note; - alias = sources_notes_display[0].name; - source_id = user_note.id; - dest_id = special_note; - reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); - if (given_reason.length > 0) - reason += " (" + given_reason + ")"; + fill_note() + }) +}) + +$('#btn_transfer').click(function () { + if (LOCK) { return } + + LOCK = true + + let error = false + + const amount_field = $('#amount') + amount_field.removeClass('is-invalid') + $('#amount-required').html('') + + const reason_field = $('#reason') + reason_field.removeClass('is-invalid') + $('#reason-required').html('') + + if (!amount_field.val() || isNaN(amount_field.val()) || amount_field.val() <= 0) { + amount_field.addClass('is-invalid') + $('#amount-required').html('<strong>Ce champ est requis et doit comporter un nombre décimal strictement positif.</strong>') + error = true + } + + const amount = Math.floor(100 * amount_field.val()) + if (amount > 2147483647) { + amount_field.addClass('is-invalid') + $('#amount-required').html('<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>') + error = true + } + + if (!reason_field.val()) { + reason_field.addClass('is-invalid') + $('#reason-required').html('<strong>Ce champ est requis.</strong>') + error = true + } + + if (!sources_notes_display.length && !$('#type_credit').is(':checked')) { + $('#source_note').addClass('is-invalid') + error = true + } + + if (!dests_notes_display.length && !$('#type_debit').is(':checked')) { + $('#dest_note').addClass('is-invalid') + error = true + } + + if (error) { + LOCK = false + return + } + + let reason = reason_field.val() + + if ($('#type_transfer').is(':checked')) { + // We copy the arrays to ensure that transactions are well-processed even if the form is reset + [...sources_notes_display].forEach(function (source) { + [...dests_notes_display].forEach(function (dest) { + if (source.note.id === dest.note.id) { + addMsg('Attention : la transaction de ' + pretty_money(amount) + ' de la note ' + source.name + + ' vers la note ' + dest.name + " n'a pas été faite car il s'agit de la même note au départ" + + " et à l'arrivée.", 'warning', 10000) + LOCK = false + return } - $.post("/api/note/transaction/transaction/", + + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: source.quantity * dest.quantity, + amount: amount, + reason: reason, + valid: true, + polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'Transaction', + source: source.note.id, + source_alias: source.name, + destination: dest.note.id, + destination_alias: dest.name + }).done(function () { + if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { + addMsg('Attention : la note émettrice ' + source.name + " n'est plus adhérente.", + 'danger', 30000) + } + if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { + addMsg('Attention : la note destination ' + dest.name + " n'est plus adhérente.", + 'danger', 30000) + } + + if (!isNaN(source.note.balance)) { + const newBalance = source.note.balance - source.quantity * dest.quantity * amount + if (newBalance <= -5000) { + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + + source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' + + 'mais la note émettrice est en négatif sévère.', 'danger', 10000) + reset() + return + } else if (newBalance < 0) { + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + + source.name + ' vers la note ' + dest.name + ' a été fait avec succès, ' + + 'mais la note émettrice est en négatif.', 'warning', 10000) + reset() + return + } + } + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + + ' vers la note ' + dest.name + ' a été fait avec succès !', 'success', 10000) + + reset() + }).fail(function (err) { // do it again but valid = false + const errObj = JSON.parse(err.responseText) + if (errObj.non_field_errors) { + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + + ' vers la note ' + dest.name + ' a échoué : ' + errObj.non_field_errors, 'danger') + LOCK = false + return + } + + $.post('/api/note/transaction/transaction/', { - "csrfmiddlewaretoken": CSRF_TOKEN, - "quantity": 1, - "amount": amount, - "reason": reason, - "valid": true, - "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, - "resourcetype": "SpecialTransaction", - "source": source_id, - "source_alias": sources_notes_display.length ? alias : null, - "destination": dest_id, - "destination_alias": dests_notes_display.length ? alias : null, - "last_name": $("#last_name").val(), - "first_name": $("#first_name").val(), - "bank": $("#bank").val() + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: source.quantity * dest.quantity, + amount: amount, + reason: reason, + valid: false, + invalidity_reason: 'Solde insuffisant', + polymorphic_ctype: TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'Transaction', + source: source.note.id, + source_alias: source.name, + destination: dest.note.id, + destination_alias: dest.name }).done(function () { - addMsg("Le crédit/retrait a bien été effectué !", "success", 10000); - if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) - addMsg("Attention : la note " + alias + " n'est plus adhérente.", "danger", 10000); - reset(); - }).fail(function (err) { - let errObj = JSON.parse(err.responseText); - let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"] - if (!error) - error = err.responseText; - addMsg("Le crédit/retrait a échoué : " + error, "danger", 10000); - LOCK = false; - }); + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + + ' vers la note ' + dest.name + ' a échoué : Solde insuffisant', 'danger', 10000) + reset() + }).fail(function (err) { + const errObj = JSON.parse(err.responseText) + let error = errObj.detail ? errObj.detail : errObj.non_field_errors + if (!error) { error = err.responseText } + addMsg('Le transfert de ' + + pretty_money(source.quantity * dest.quantity * amount) + ' de la note ' + source.name + + ' vers la note ' + dest.name + ' a échoué : ' + error, 'danger') + LOCK = false + }) + }) + }) + }) + } else if ($('#type_credit').is(':checked') || $('#type_debit').is(':checked')) { + let special_note + let user_note + let alias + const given_reason = reason + let source_id, dest_id + if ($('#type_credit').is(':checked')) { + special_note = $('#credit_type').val() + user_note = dests_notes_display[0].note + alias = dests_notes_display[0].name + source_id = special_note + dest_id = user_note.id + reason = 'Crédit ' + $('#credit_type option:selected').text().toLowerCase() + if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } + } else { + special_note = $('#debit_type').val() + user_note = sources_notes_display[0].note + alias = sources_notes_display[0].name + source_id = user_note.id + dest_id = special_note + reason = 'Retrait ' + $('#credit_type option:selected').text().toLowerCase() + if (given_reason.length > 0) { reason += ' (' + given_reason + ')' } } -}); + $.post('/api/note/transaction/transaction/', + { + csrfmiddlewaretoken: CSRF_TOKEN, + quantity: 1, + amount: amount, + reason: reason, + valid: true, + polymorphic_ctype: SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, + resourcetype: 'SpecialTransaction', + source: source_id, + source_alias: sources_notes_display.length ? alias : null, + destination: dest_id, + destination_alias: dests_notes_display.length ? alias : null, + last_name: $('#last_name').val(), + first_name: $('#first_name').val(), + bank: $('#bank').val() + }).done(function () { + addMsg('Le crédit/retrait a bien été effectué !', 'success', 10000) + if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg('Attention : la note ' + alias + " n'est plus adhérente.", 'danger', 10000) } + reset() + }).fail(function (err) { + const errObj = JSON.parse(err.responseText) + let error = errObj.detail ? errObj.detail : errObj.non_field_errors + if (!error) { error = err.responseText } + addMsg('Le crédit/retrait a échoué : ' + error, 'danger', 10000) + LOCK = false + }) + } +}) diff --git a/tox.ini b/tox.ini index b160324b219b973e4097b1949ac6da2d96809fb3..eff97a2badec3cb0228626d635d29f10a9d0a0a0 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ deps = -r{toxinidir}/requirements.txt coverage commands = - ./manage.py makemigrations coverage run --omit='*migrations*,apps/scripts*' --source=apps,note_kfet ./manage.py test apps/ coverage report -m